SwiftData使用含可选关联链的KeyPath排序查询时崩溃
问题背景
定义了以下SwiftData模型:
@Model class Game { var name: String var firstReleasedOn: Date? @Relationship(deleteRule: .cascade, inverse: \QueueEntry.game) var queueEntry: QueueEntry? init( name: String, firstReleasedOn: Date? ) { self.name = name self.firstReleasedOn = firstReleasedOn } }
@Model public class QueueEntry { var game: Game? var createdOn: Date var order: Int init(order: Int) { self.createdOn = .now self.order = order } }
为按游戏发布日期排序展示所有QueueEntry数据,编写了如下视图代码:
struct QueueView: View { @Query(sort: [SortDescriptor(\QueueEntry.game?.firstReleasedOn)]) private var entries: [LocalData.QueueEntry] var body: some View { List(entries) { entry in Text(entry.game.name ?? "") } } }
该代码在Debug模式下运行正常,但在Release模式下抛出致命错误:
SwiftData/DataUtilities.swift:65: Fatal error: Couldn't find \QueueEntry.<computed 0x00000001049cd3e0 (Optional)>?.<computed 0x00000001049cd440 (Optional)> on QueueEntry with fields [SwiftData.Schema.PropertyMetadata(name: "game", keypath: \QueueEntry.<computed 0x00000001049e3814 (Optional)>, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "createdOn", keypath: \QueueEntry.<computed 0x00000001049e40e8 (Date)>, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "order", keypath: \QueueEntry.<computed 0x00000001049e49ac (Int)>, defaultValue: nil, metadata: nil)]
由于将QueueEntry.game设为非可选并在初始化时创建关联会触发NSInvalidArgumentException,因此该属性必须保持可选类型。
问题分析
这大概率是SwiftData的框架bug。Debug模式下编译器保留了更多keypath元数据,可解析嵌套的可选关联属性排序;但Release模式下的优化会移除这些元数据,导致SwiftData无法识别\QueueEntry.game?.firstReleasedOn这类嵌套可选keypath,进而触发崩溃。
替代解决方案
方案1:添加计算属性用于排序
在QueueEntry中新增计算属性,返回关联游戏的发布日期,基于该属性完成排序:
@Model public class QueueEntry { var game: Game? var createdOn: Date var order: Int // 新增计算属性 var gameReleaseDate: Date? { game?.firstReleasedOn } init(order: Int) { self.createdOn = .now self.order = order } }
修改视图中的@Query排序规则:
struct QueueView: View { @Query(sort: [SortDescriptor(\QueueEntry.gameReleaseDate)]) private var entries: [LocalData.QueueEntry] var body: some View { List(entries) { entry in Text(entry.game.name ?? "") } } }
方案2:内存中手动排序
若不想修改模型,可在获取QueueEntry后手动在内存中完成排序逻辑:
struct QueueView: View { @Query private var entries: [LocalData.QueueEntry] // 手动排序后的结果数组 private var sortedEntries: [LocalData.QueueEntry] { entries.sorted { guard let date1 = $0.game?.firstReleasedOn, let date2 = $1.game?.firstReleasedOn else { // 处理无发布日期的情况,示例:将有日期的排在前面 return $0.game?.firstReleasedOn != nil } return date1 < date2 } } var body: some View { List(sortedEntries) { entry in Text(entry.game.name ?? "") } } }
方案3:保证关联完整性的业务约束
为确保QueueEntry始终关联有效的Game(即使属性为可选),可新增强制传入Game的初始化方法,在业务层面约束关联关系:
@Model public class QueueEntry { var game: Game? var createdOn: Date var order: Int // 强制关联Game的初始化方法 init(game: Game, order: Int) { self.game = game self.createdOn = .now self.order = order // 同步设置反向关联 game.queueEntry = self } // 保留原初始化方法(按需使用) init(order: Int) { self.createdOn = .now self.order = order } }
该方式既避开了非可选属性初始化时的NSInvalidArgumentException,又能在业务逻辑层面保证QueueEntry都有对应的Game关联。
内容的提问来源于stack exchange,提问作者Ben Walker

