SwiftData复杂Predicate编译错误及组合查询返回空结果问题
背景
之前编写SwiftData的日期Predicate时遇到编译超时错误,通过给Optional添加Comparable扩展解决:
extension Optional: Comparable where Wrapped: Comparable { public static func < (lhs: Wrapped?, rhs: Wrapped?) -> Bool { guard let lhs, let rhs else { return false } return lhs < rhs } }
但给@Model类添加可选Bool属性var isActive: Bool?后,再次出现相同编译错误:
let aPred = #Predicate<TestEvent> { $0.isActive == true && // 注释此行即可编译 $0.dateStart != nil && ($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) || ($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) || ($0.dateStart < startOfDay && $0.dateEnd >= startOfDay) }
约束条件:
- 必须使用SwiftData,不能替换为CoreData
- 所有属性需保持可选以支持CloudKit
- 不能强制解包,nil是有效场景
理想方案是拆分Predicate并组合,类似CoreData的NSCompoundPredicate。
问题重现(Xcode 15.4)
升级Xcode后,拆分Predicate并用.conjunction()组合,代码可编译但查询返回空结果:
func mainPred(_ aDate: Date) -> Predicate<Event> { let startOfDay = aDate.startOfDay let endOfDay = aDate.endOfDay let datesPred = #Predicate<Event> { $0.dateStart != nil && ($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) || ($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) || ($0.dateStart < startOfDay && $0.dateEnd >= startOfDay) } let activePred = #Predicate<Event> { $0.isActive == true } return [activePred, datesPred].conjunction() } func fetchData() { var events = [Event]() let descriptor = FetchDescriptor<Event>(predicate: mainPred(aDate)) do { events = try context.fetch(descriptor) } catch { print(error) } // events为空 }
但如果仅使用日期Predicate查询,再对结果数组用$0.isActive == true过滤,就能得到正确结果:
func mainPred(_ aDate: Date) -> Predicate<Event> { let startOfDay = aDate.startOfDay let endOfDay = aDate.endOfDay let datesPred = #Predicate<Event> { $0.dateStart != nil && ($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) || ($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) || ($0.dateStart < startOfDay && $0.dateEnd >= startOfDay) } return datesPred } func fetchData() { var events = [Event]() let descriptor = FetchDescriptor<Event>(predicate: mainPred(aDate)) do { events = try context.fetch(descriptor) } catch { print(error) } events = events.filter { $0.isActive == true } // events包含预期结果 }
问题分析与解决
1. 可选Bool的Predicate写法问题
SwiftData的#Predicate对可选Bool的== true处理和内存中的filter逻辑不一致。内存中$0.isActive == true会匹配isActive为true的对象,但SwiftData查询引擎需要明确判断非nil且为true,可修改为:
let activePred = #Predicate<Event> { $0.isActive == .some(true) }
或更明确的写法:
let activePred = #Predicate<Event> { $0.isActive != nil && $0.isActive == true }
2. 日期Predicate的逻辑优先级问题
原日期Predicate的逻辑存在优先级歧义:&&优先级高于||,导致实际逻辑是($0.dateStart != nil && 第一个日期条件) || 第二个日期条件 || 第三个日期条件,这会让dateStart为nil但后两个日期条件满足的对象被匹配,和内存过滤逻辑不符。
需将$0.dateStart != nil与整个日期条件组用括号包裹,确保只有dateStart非nil时才判断后续条件:
let datesPred = #Predicate<Event> { $0.dateStart != nil && ( ($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) || ($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) || ($0.dateStart < startOfDay && $0.dateEnd >= startOfDay) ) }
3. 组合Predicate的写法兼容性
Xcode 15.4的SwiftData对数组.conjunction()方法的支持可能存在bug,可手动创建CompoundPredicate替代:
return CompoundPredicate(type: .and, subpredicates: [activePred, datesPred])
验证方案
结合上述修改后的完整代码:
func mainPred(_ aDate: Date) -> Predicate<Event> { let startOfDay = aDate.startOfDay let endOfDay = aDate.endOfDay let datesPred = #Predicate<Event> { $0.dateStart != nil && ( ($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) || ($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) || ($0.dateStart < startOfDay && $0.dateEnd >= startOfDay) ) } let activePred = #Predicate<Event> { $0.isActive == .some(true) } return CompoundPredicate(type: .and, subpredicates: [activePred, datesPred]) }
重新执行查询即可得到预期结果。
内容的提问来源于stack exchange,提问作者grep

