You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 13:10:25