SwiftData @Query使用枚举或关联关系筛选/排序时崩溃
SwiftData @Query 崩溃问题:自定义枚举/可转换类型处理方案
问题重现
当使用SwiftData的@Query对非标准支持的数据类型(如自定义枚举、带值转换器的UIColor)进行筛选或排序时,会触发致命崩溃,报错信息:
SwiftData/DataUtilities.swift:1140: Fatal error: Unexpected type for Expansion: Optional<UIColor>
崩溃发生在访问_items.wrappedValue时,仅使用字符串、布尔值、日期等基础类型时无异常,推测是SwiftData的底层查询解析bug。
涉及的核心场景:
- 基于可选枚举
Priority的排序/筛选 - 基于关联对象
Category的链式属性访问(如Item.category?.title) - 关联对象中包含带值转换器的
UIColor属性
原因分析
SwiftData当前对以下场景的查询解析存在缺陷:
- 可选枚举类型的直接比较、链式rawValue访问
- 可选关联对象的链式属性访问(如
?.title) - 带值转换器的可选类型(如
Optional<UIColor>)会干扰查询解析流程,即使查询未直接涉及该属性
解决方案
1. 枚举类型的排序与筛选优化
排序处理
避免直接对可选枚举的rawValue创建SortDescriptor,改为在模型中添加计算属性统一处理nil情况:
@Model final class Item: Codable { // ... 原有属性 ... var priority: Priority? // 添加计算属性,给nil枚举一个默认rawValue var prioritySortValue: Int { priority?.rawValue ?? -1 // nil值会排在所有枚举值之前 } }
排序时使用该计算属性:
func sort() -> [SortDescriptor<Item>]{ switch self { // ... 其他case ... case .priority: [SortDescriptor(\Item.prioritySortValue)] } }
筛选处理
将枚举比较转为rawValue的数值比较,避免直接比较枚举实例:
let highPriority = Priority.high if let query { return #Predicate { $0.priority?.rawValue == highPriority.rawValue && ($0.title.contains(query) || $0.categoryTitle.contains(query)) && $0.isArchived == false } }
2. 关联属性链式访问优化
对Item.category?.title的访问,同样通过计算属性消除可选链式调用:
@Model final class Item: Codable { // ... 原有属性 ... @Relationship(deleteRule: .nullify, inverse: \Category.items) var category: Category? // 添加计算属性,处理category为nil的情况 var categoryTitle: String { category?.title ?? "" } }
排序和筛选时直接使用该计算属性:
// 排序 case .category: [SortDescriptor(\Item.categoryTitle)] // 筛选 $0.categoryTitle.contains(query)
3. 移除值转换器改用基础类型存储
针对Category中的UIColor属性,放弃使用值转换器,改为存储Hex字符串(基础类型),通过计算属性转换为UIColor:
@Model class Category: Codable { @Attribute(.unique) var title: String var items: [Item]? // 存储Hex字符串,替代值转换器 var colorHex: String? // 计算属性处理UIColor与Hex的转换 var color: UIColor? { get { guard let hex = colorHex else { return nil } let scanner = Scanner(string: hex.replacingOccurrences(of: "#", with: "")) var hexNumber: UInt64 = 0 guard scanner.scanHexInt64(&hexNumber) else { return nil } let r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 let g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 let b = CGFloat(hexNumber & 0x0000ff) / 255 return UIColor(red: r, green: g, blue: b, alpha: 1) } set { guard let color = newValue else { colorHex = nil return } var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 color.getRed(&r, green: &g, blue: &b, alpha: &a) let hex = String(format: "#%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255))) colorHex = hex } } // 调整初始化方法 init(title: String = "", color: UIColor) { self.title = title self.color = color } // Codable实现调整(仅处理title和colorHex) enum CodingKeys: String, CodingKey { case title, colorHex } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.title = try container.decode(String.self, forKey: .title) self.colorHex = try container.decodeIfPresent(String.self, forKey: .colorHex) ?? UIColor(possibleColors.randomElement()!)?.hexString } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(colorHex, forKey: .colorHex) } }
这种方式完全使用SwiftData原生支持的字符串类型存储颜色,避免值转换器带来的查询解析问题。
验证结果
修改后,基于枚举、关联属性的排序和筛选操作均能正常执行,不再触发崩溃。
内容的提问来源于stack exchange,提问作者tundsdev
相关产品推荐
相关产品推荐

