You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftData中ForEach报重复ID错误及分类父子关系疑问

问题:SwiftData分类模型重复ID错误与父子关系定义疑问

1. 定义的Category模型

@Model
final class Category {
    @Attribute(.unique) var id: UUID
    var name: String
    var parent_id: UUID //categories can be children of other categories

    init(id: UUID, name: String, parent_id: UUID) {
        self.id = id
        self.name = name
        self.parent_id = parent_id
    }
}

2. 分类数据展示View代码

import SwiftUI
import SwiftData

struct CategoryView: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query private var categories: [Category]
    @Query(filter: #Predicate<Category>{ $0.parent_id == nil })
        private var top_level_categories: [Category]
    
    var spacing: CGFloat = 25

    var body: some View {
        HStack() {
            Text("Categories")
                .font(.title.bold())
            
            Spacer()
            Text("see all")
        }
        .padding([.bottom, .top], 0)
        VStack(spacing: 20) {
            ScrollView(.horizontal) {
                HStack(spacing: spacing) {
                    ForEach(top_level_categories) { category in
                        Text(category.name!)

                    }
                }
            }
        }
        .onAppear{
            getCategories()
        }
    }
    
    func getCategories()  {
        get_refresh_token { token in
            guard let token = token else {
                return
            }
            var urlRequest = URLRequest (url: URL(string:"https://api.test.com/categories")!)
            urlRequest.httpMethod = "GET"
            urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
            urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            
            URLSession.shared
                .dataTask(with: urlRequest) { (data, response, error) in
                    do {
                        if let data = data {
                            let c = try JSONDecoder().decode([Category].self, from: data)
                            c.forEach { modelContext.insert($0) }
                            try? modelContext.save()
                        }
                    }
                    catch {
                        print(error)
                    }
                }.resume()
            
        }
    }
}

3. 遇到的错误

ForEach<Array<Category>, UUID, Text>: the ID XXXXXX-XXXX-XXXX-XXXX-XXXXXX occurs multiple times within the collection, this will give undefined results!

4. 疑问与需求

  • 已经给id添加@Attribute(.unique)约束,为什么再次运行会出现重复ID错误?
  • 当前父子关系的定义是否正确?
  • 需求:筛选无parent_id的顶级分类,以及获取指定分类的子分类。

问题解决与优化方案

一、重复ID错误的原因与修复

@Attribute(.unique)只是约束数据库中不能存在重复ID的记录,但你每次调用getCategories()时,都会把API返回的所有Category实例直接插入模型上下文——哪怕数据库里已经有相同ID的记录,这会导致内存上下文里出现重复实例,ForEach遍历自然会报ID重复。

修复方法:插入前先检查数据库中是否已存在该ID的分类,不存在再插入,同时要注意在主线程操作模型上下文(SwiftData默认上下文必须在主线程使用):

// 修改getCategories中的数据处理逻辑
URLSession.shared
    .dataTask(with: urlRequest) { (data, response, error) in
        DispatchQueue.main.async { // 切换到主线程操作上下文
            do {
                if let data = data {
                    let c = try JSONDecoder().decode([Category].self, from: data)
                    c.forEach { category in
                        // 查询数据库中是否已有相同ID的分类
                        let existing = try? modelContext.fetch(
                            FetchDescriptor<Category>(predicate: #Predicate { $0.id == category.id })
                        ).first
                        if existing == nil {
                            modelContext.insert(category)
                        }
                    }
                    try? modelContext.save()
                }
            } catch {
                print(error)
            }
        }
    }.resume()

额外优化:避免每次View出现都请求API,添加判断逻辑:

.onAppear{
    if categories.isEmpty { // 数据库无数据时才请求
        getCategories()
    }
}

二、父子关系的正确定义

当前模型中parent_id是非可选类型,顶级分类的parent_id无法设为nil,导致你写的$0.parent_id == nil筛选逻辑根本不会生效。同时手动管理parent_idUUID不够直观,建议改用SwiftData的关系属性来定义父子关联:

@Model
final class Category {
    @Attribute(.unique) var id: UUID
    var name: String
    weak var parent: Category? // 父分类,弱引用避免循环引用
    @Relationship(deleteRule: .cascade, inverse: \Category.parent) 
    var children: [Category] = [] // 子分类数组

    init(id: UUID, name: String, parent: Category? = nil) {
        self.id = id
        self.name = name
        self.parent = parent
    }
}

这样修改后:

  • 筛选顶级分类的Query可以改成:
    @Query(filter: #Predicate<Category>{ $0.parent == nil })
    private var top_level_categories: [Category]
    
  • 获取指定分类的子分类直接调用category.children即可,无需手动处理UUID关联。

如果坚持要用parent_id的方式,至少要把它改成可选类型:

var parent_id: UUID?
// 对应初始化方法调整
init(id: UUID, name: String, parent_id: UUID? = nil) {
    self.id = id
    self.name = name
    self.parent_id = parent_id
}

内容的提问来源于stack exchange,提问作者radishhorse

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 21:30:24