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
相关产品推荐
相关产品推荐

