SwiftData查询结果异常:iPhone14 Pro返回37条,iPhone13返回0条
SwiftData跨iOS版本查询结果不一致问题排查与解决
问题描述
在运行iOS17.3的iPhone14 Pro上,通过FetchDescriptor查询Category实体(条件为isActive == true且woozles.count > 2)返回37条结果;但在iOS17.3.1的iPhone13及iOS17.2的iPhone13模拟器上,相同代码返回0条结果。视图中使用@Query能正常获取37条数据,但业务需求要求在ViewModel中执行查询,需解决该版本差异问题。
查询代码
let categoryFetchDescriptor = FetchDescriptor<Category>(predicate: #Predicate { $0.isActive == true && $0.woozles.count > 2 }, sortBy: [SortDescriptor(\.sortPriority, order: .forward)]) let allCategories = try! modelContext.fetch(categoryFetchDescriptor) print("fetched active categories: \(allCategories.count)") //37 on one device, 0 on the other
Category实体定义
@Model class Category: Decodable, Identifiable, ObservableObject { var id: Int var title: String var isActive: Bool @Relationship var woozles = [Woozle]() ... }
问题原因
SwiftData在不同iOS版本中对关系属性的谓词解析存在兼容性bug:
- iOS17.2、17.3.1版本无法正确解析
$0.woozles.count > 2这种直接访问关联集合数量的条件,导致查询逻辑失效返回空结果。 @Query内部采用了不同的查询逻辑或缓存机制,规避了该谓词解析bug,因此能正常返回数据。
解决办法
方式一:使用子查询替代直接访问count
通过@Subquery明确统计关联实体数量,兼容所有iOS17版本:
let categoryFetchDescriptor = FetchDescriptor<Category>(predicate: #Predicate { $0.isActive == true && @Subquery($0.woozles).count > 2 }, sortBy: [SortDescriptor(\.sortPriority, order: .forward)])
方式二:内存中手动过滤
先查询所有符合isActive == true的Category,再在内存中过滤关联集合数量符合要求的项(适合数据量较小的场景):
let baseFetchDescriptor = FetchDescriptor<Category>(predicate: #Predicate { $0.isActive == true }, sortBy: [SortDescriptor(\.sortPriority, order: .forward)]) let allActiveCategories = try! modelContext.fetch(baseFetchDescriptor) let filteredCategories = allActiveCategories.filter { $0.woozles.count > 2 } print("fetched active categories: \(filteredCategories.count)")
方式三:添加持久化计数属性(性能最优)
在Category实体中新增持久化的woozleCount属性,同步关联集合的数量变化,查询时直接使用该属性:
@Model class Category: Decodable, Identifiable, ObservableObject { var id: Int var title: String var isActive: Bool var woozleCount: Int = 0 @Relationship(deleteRule: .cascade, inverse: \Woozle.category) var woozles = [Woozle]() @Transient private var cancellables = Set<AnyCancellable>() init() { $woozles .map { $0.count } .assign(to: \.woozleCount, on: self) .store(in: &cancellables) } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) title = try container.decode(String.self, forKey: .title) isActive = try container.decode(Bool.self, forKey: .isActive) woozles = try container.decode([Woozle].self, forKey: .woozles) woozleCount = woozles.count } enum CodingKeys: String, CodingKey { case id, title, isActive, woozles } }
查询谓词修改为:
#Predicate { $0.isActive == true && $0.woozleCount > 2 }
内容的提问来源于stack exchange,提问作者soleil
相关产品推荐
相关产品推荐

