使用多SortDescriptor的@Query导致SwiftData崩溃问题咨询
SwiftData 关联属性排序崩溃问题
环境
- Xcode Version 15.0 beta (15A5160n)
- 测试环境:macOS Sonoma beta、iOS 17.0 iPhone 14 Pro模拟器
重现步骤
- 将下方代码导入SwiftData新项目
- 构建并运行应用
- 首次运行点击「Setup」按钮初始化SwiftData容器数据
- 当前
@Query仅按Attribute.order排序,可正常展示所有Attribute对象 - 注释单条件排序的
@Query,取消注释包含两个SortDescriptor的@Query(按Attribute.item.order和Attribute.order排序) - 重新运行应用直接崩溃,控制台报错:
SwiftData/DataUtilities.swift:1179: Fatal error: Unexpected type for Expansion: Item
预期效果
Attribute按所属Item的Item.order分组,组内按Attribute.order排序,展示格式如下:
Item[0] Attribute[0] Item[0] Attribute[1] Item[0] Attribute[2] Item[1] Attribute[0] Item[1] Attribute[1] Item[1] Attribute[2] Item[2] Attribute[0] Item[2] Attribute[1] Item[2] Attribute[2]
疑问
该排序模式在Core Data中可正常工作,请问此用法是否合理?有无可行的解决方案?
测试代码
import SwiftUI import SwiftData @Model final class Item { var name: String var order: Int @Relationship(.cascade) var attributes: [Attribute] = [] init(name: String, order: Int) { self.name = name self.order = order } } @Model final class Attribute { var name: String var order: Int @Relationship var item: Item init(item: Item, name: String, order: Int) { self.item = item self.name = name self.order = order } } struct ContentView: View { @Environment(.modelContext) private var modelContext @Query(FetchDescriptor<Attribute>(sortBy: [SortDescriptor(\Attribute.order, order: .forward)])) // @Query(FetchDescriptor<Attribute>(sortBy: [SortDescriptor(\Attribute.item.order, order: .forward), // SortDescriptor(\Attribute.order, order: .forward)])) private var attributes: [Attribute] var body: some View { NavigationView { List { ForEach(self.attributes) { attribute in Text("Item[\(attribute.item.order)] Attribute[\(attribute.order)]") .monospaced() } } .toolbar { ToolbarItem { Button("Setup", action: self.setup) } } } } private func setup() { withAnimation { do { let attributeDescriptor = FetchDescriptor<Attribute>() let attributes = try self.modelContext.fetch(attributeDescriptor) for attribute in attributes { self.modelContext.delete(attribute) } let itemDescriptor = FetchDescriptor<Item>() let items = try self.modelContext.fetch(itemDescriptor) for item in items { self.modelContext.delete(item) } try self.modelContext.save() let item1 = Item(name: "Z", order: 0) self.modelContext.insert(Attribute(item: item1, name: "\(item1.name).0", order: 0)) self.modelContext.insert(Attribute(item: item1, name: "\(item1.name).1", order: 1)) self.modelContext.insert(Attribute(item: item1, name: "\(item1.name).2", order: 2)) self.modelContext.insert(item1) let item2 = Item(name: "Y", order: 1) self.modelContext.insert(Attribute(item: item2, name: "\(item2.name).0", order: 0)) self.modelContext.insert(Attribute(item: item2, name: "\(item2.name).1", order: 1)) self.modelContext.insert(Attribute(item: item2, name: "\(item2.name).2", order: 2)) self.modelContext.insert(item2) let item3 = Item(name: "X", order: 2) self.modelContext.insert(Attribute(item: item3, name: "\(item3.name).0", order: 0)) self.modelContext.insert(Attribute(item: item3, name: "\(item3.name).1", order: 1)) self.modelContext.insert(Attribute(item: item3, name: "\(item3.name).2", order: 2)) self.modelContext.insert(item2) try self.modelContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } } }
解决方案与分析
用法合理性
你的用法逻辑上完全合理,这是关联属性排序的常见业务场景,Core Data支持此类操作也证明了需求的合法性。当前SwiftData的崩溃属于beta版本的已知bug,你提交反馈的做法是正确的。
可行解决方案
目前有三种方式可以绕过这个bug:
方案1:内存中二次排序
保持原单条件@Query获取所有Attribute数据,在视图中通过计算属性完成内存排序:
@Query(FetchDescriptor<Attribute>(sortBy: [SortDescriptor(\Attribute.order, order: .forward)])) private var attributes: [Attribute] // 计算属性实现目标排序逻辑 private var sortedAttributes: [Attribute] { attributes.sorted { if $0.item.order != $1.item.order { return $0.item.order < $1.item.order } return $0.order < $1.order } } // 视图中替换为sortedAttributes List { ForEach(sortedAttributes) { attribute in Text("Item[\(attribute.item.order)] Attribute[\(attribute.order)]") .monospaced() } }
方案2:反向查询+嵌套展示
通过查询Item并按Item.order排序,再嵌套展示其关联的Attribute(组内按Attribute.order排序),这种方式更贴合SwiftData的关联数据设计思路:
// 修改查询对象为Item @Query(FetchDescriptor<Item>(sortBy: [SortDescriptor(\Item.order, order: .forward)])) private var items: [Item] // 视图中嵌套展示分组数据 List { ForEach(items) { item in Section(header: Text("Item[\(item.order)]")) { ForEach(item.attributes.sorted(by: { $0.order < $1.order })) { attribute in Text("Attribute[\(attribute.order)]") .monospaced() } } } }
方案3:显式预加载关联对象(进阶)
如果需要在数据库层面完成排序,可以尝试手动构建FetchDescriptor并显式指定预加载关联的Item,部分beta版本中此方法可规避崩溃:
// 自定义FetchDescriptor var fetchDescriptor: FetchDescriptor<Attribute> { let descriptor = FetchDescriptor<Attribute>() descriptor.sortBy = [ SortDescriptor(\Attribute.item.order), SortDescriptor(\Attribute.order) ] // 显式预加载关联的Item,避免延迟加载引发的类型错误 descriptor.relationshipKeyPathsForPrefetching = [\Attribute.item] return descriptor } @Query(fetchDescriptor) private var attributes: [Attribute]
内容的提问来源于stack exchange,提问作者Chuck H
相关产品推荐
相关产品推荐

