SwiftData — FetchDescriptor

FetchDescriptor 是苹果在 Swift Data 框架(WWDC23 推出)中引入的一个关键类型,用于定义数据查询的配置(过滤、排序、分页等)。它是 Swift Data 中替代 Core Data 的 NSFetchRequest 的现代化方案,完全基于 Swift 并发模型设计,支持类型安全和 Swift 原生语法。


一、核心功能 #

功能说明
数据过滤通过 Predicate 宏定义查询条件(类型安全,支持 Swift 原生类型)
数据排序使用 SortDescriptor 定义排序规则(支持多字段排序)
分页查询通过 fetchLimitoffset 实现分页加载
关系预加载通过 relationshipsToPrefetch 预加载关联数据,优化性能
结果去重使用 propertiesToFetch 指定返回字段,结合 returnsDistinctResults 去重
变更跟踪通过 includePendingChanges 控制是否包含未保存的临时数据

二、基本用法 #

1. 简单查询 #

// 查询所有 User 对象
let descriptor = FetchDescriptor<User>()
let users = try await context.fetch(descriptor)

2. 过滤 + 排序 #

// 查询年龄 >= 18 的用户,按姓名升序排列
let predicate = #Predicate<User> { $0.age >= 18 }
let sort = SortDescriptor(\User.name, order: .forward)

let descriptor = FetchDescriptor(
  predicate: predicate,
  sortBy: [sort]
)

3. 分页查询 #

// 每页 20 条数据,加载第 3 页
let descriptor = FetchDescriptor<User>(
  fetchLimit: 20,
  offset: 40  // (3-1)*20 = 40
)

4. 预加载关联数据 #

// 预加载用户的订单数据
let descriptor = FetchDescriptor<User>(
  relationshipsToPrefetch: [\.orders]
)

三、对比 Core Data 的 NSFetchRequest #

特性FetchDescriptor (Swift Data)NSFetchRequest (Core Data)
类型安全✅ 基于 Swift 泛型和 KeyPath❌ 依赖字符串 Key
并发支持✅ 原生支持 Swift async/await❌ 需手动管理线程上下文
语法简洁性✅ 使用 Swift 宏 (#Predicate)❌ 依赖 NSPredicate 字符串
关系处理✅ 直接通过 KeyPath 访问关联对象❌ 需要手动配置 relationshipKeyPaths
内存优化✅ 自动优化延迟加载✅ 类似但需手动配置

四、高级用法示例 #

1. 动态排序 #

// 根据用户选择动态切换排序字段
func dynamicSortDescriptor(by keyPath: KeyPath<User, String>) -> FetchDescriptor<User> {
  FetchDescriptor(sortBy: [SortDescriptor(keyPath, order: .forward)])
}

2. 聚合查询 #

// 计算用户平均年龄
let descriptor = FetchDescriptor<User>(
  propertiesToFetch: [\.age]
)
let ages = try await context.fetch(descriptor)
let averageAge = ages.compactMap { $0.age }.reduce(0, +) / ages.count

3. 去重查询 #

// 获取所有不重复的用户所在城市
let descriptor = FetchDescriptor<User>(
  propertiesToFetch: [\.city],
  returnsDistinctResults: true
)

五、性能优化建议 #

  1. 索引加速:对频繁查询的字段添加 @Attribute(.unique)@Attribute(.indexed)
  2. 批量加载:合理使用 fetchLimit + offset 避免一次性加载过多数据
  3. 选择性加载:通过 propertiesToFetch 仅加载必要字段
  4. 内存优化:设置 includesPropertyValues = false 当仅需对象引用时 so

@Query #

在 SwiftData 中,@Query 属性包装器用于在 SwiftUI 视图中声明式地管理和自动更新数据查询。结合 FetchDescriptor,可以灵活配置查询条件、排序、分页等。以下是使用 @Queryinit(fetchDescriptor:animation:) 初始化器的详细说明和示例:


基本用法 #

通过 FetchDescriptor 定义查询条件,并指定数据变化时的动画效果:

import SwiftData
import SwiftUI

// 定义数据模型
@Model
final class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// SwiftUI 视图中使用 @Query
struct PersonListView: View {
    // 使用 FetchDescriptor 初始化查询
    @Query(
        fetchDescriptor: FetchDescriptor<Person>(
            predicate: #Predicate { $0.age >= 18 },  // 过滤条件:年龄 ≥18
            sortBy: [SortDescriptor(\.name, order: .forward)],  // 按姓名升序
            fetchLimit: 100  // 最多获取 100 条
        ),
        animation: .bouncy  // 数据变化时的动画效果
    )
    private var adults: [Person]

    var body: some View {
        List(adults) { person in
            Text("\(person.name), \(person.age)")
        }
    }
}

关键参数解析 #

1. FetchDescriptor<Element> #

  • predicate:使用 #Predicate 宏定义过滤条件(Swift 5.9+)。
    predicate: #Predicate { $0.age > 20 && $0.name.contains("A") }
    
  • sortBy:指定排序方式,支持多个字段。
    sortBy: [SortDescriptor(\.age, order: .reverse), SortDescriptor(\.name)]
    
  • fetchLimit/fetchOffset:实现分页。
    fetchLimit: 20, fetchOffset: 40  // 每页 20 条,第三页
    

2. animation: Animation #

  • 指定数据变化时的动画效果(如插入、删除、更新):
    animation: .easeInOut(duration: 0.5)  // 自定义动画
    // 或使用预设动画:
    // .default, .bouncy, .snappy, .smooth
    

动态更新查询条件 #

通过 @State 动态修改 FetchDescriptor,实现交互式过滤/排序:

struct DynamicFilterView: View {
    @State private var searchText = ""
    @State private var minAge = 18

    // 动态生成 FetchDescriptor
    private var descriptor: FetchDescriptor<Person> {
        FetchDescriptor<Person>(
            predicate: #Predicate {
                $0.age >= minAge &&
                $0.name.localizedStandardContains(searchText)
            },
            sortBy: [SortDescriptor(\.name)]
        )
    }

    // 使用动态 descriptor 初始化 Query
    @Query(animation: .smooth)
    private var filteredPeople: [Person]

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
            Slider(value: $minAge, in: 0...100, label: { Text("Min Age: \(minAge)") })
            
            List(filteredPeople) { person in
                Text("\(person.name), \(person.age)")
            }
        }
        .onChange(of: searchText) { _ in
            filteredPeople.update(with: descriptor) // 手动更新查询
        }
    }
}

注意事项 #

  1. 自动更新:默认情况下,@Query 会监听数据上下文的变化并自动刷新视图。若需手动控制,可在 FetchDescriptor 中设置 transactional: false
  2. 性能优化:对大型数据集使用 fetchLimitfetchOffset 避免内存压力。
  3. 类型安全#Predicate 会在编译时检查条件表达式,避免运行时错误。

通过结合 FetchDescriptor 的灵活配置和 SwiftUI 的动画系统,可以高效构建响应式数据驱动的界面。

propertiesToFetch #

在 SwiftData 中,FetchDescriptorpropertiesToFetch 属性用于优化数据查询性能,允许开发者指定仅获取实体(Entity)中的部分属性,而不是整个对象。这在处理大型数据集或仅需部分字段时非常有用。以下是具体用法和示例:


一、propertiesToFetch 的核心作用 #

  1. 减少数据传输:仅从持久化存储中加载指定的属性,降低内存占用。
  2. 提升查询速度:减少数据库或存储引擎需要处理的数据量。
  3. 类型安全:通过 Swift 的 KeyPath 语法指定属性,避免字符串硬编码。

二、基本语法 #

假设有一个 Animal 实体,包含 nameagespecies 属性:

@Model
class Animal {
    var name: String
    var age: Int
    var species: String
    
    init(name: String, age: Int, species: String) {
        self.name = name
        self.age = age
        self.species = species
    }
}

要仅获取 namespecies 属性,可以按如下方式配置 FetchDescriptor

import SwiftData

// 定义要获取的属性列表
let propertiesToFetch: [PartialKeyPath<Animal>] = [
    \.name,
    \.species
]

// 创建 FetchDescriptor
let descriptor = FetchDescriptor<Animal>(
    propertiesToFetch: propertiesToFetch
)

// 执行查询
let results = try? modelContext.fetch(descriptor)

三、返回的数据结构 #

当使用 propertiesToFetch 时,查询结果不再是完整的 Animal 对象,而是一个包含字典数组的结构,每个字典对应一个对象的指定属性。

示例输出结构: #

// 假设查询结果
results = [
    [name: "Lion", species: "Panthera leo"],
    [name: "Elephant", species: "Loxodonta"]
]

访问数据: #

for result in results {
    if let name = result["name"] as? String,
       let species = result["species"] as? String {
        print("Name: \(name), Species: \(species)")
    }
}

四、性能优化场景 #

  1. 列表展示:在 UITableView 或 SwiftUI List 中仅需显示部分字段(如名称和物种)。
  2. 批量处理:在后台任务中统计或处理特定属性(如所有动物的平均年龄)。
  3. 网络同步:仅同步必要的字段到服务器,减少数据传输量。

五、对比传统 Core Data 的 NSFetchRequest #

在 Core Data 中,类似功能通过 NSFetchRequestpropertiesToFetch 实现:

let request = NSFetchRequest<NSDictionary>(entityName: "Animal")
request.resultType = .dictionaryResultType
request.propertiesToFetch = ["name", "species"]

SwiftData 的优势: #

  • 类型安全:使用 KeyPath(\.name)而非字符串,避免拼写错误。
  • Swift 原生集成:与 Swift 并发模型(async/await)和 SwiftUI 更深度结合。

六、注意事项 #

  1. 类型转换:返回的字典值需要手动转换为正确类型(如 as? String)。
  2. 不可访问未获取的属性:尝试访问未在 propertiesToFetch 中指定的属性会导致运行时错误。
  3. 关联对象:如果获取的属性包含关联对象(Relationships),需谨慎处理懒加载问题。

七、完整示例代码 #

import SwiftData

// 定义 FetchDescriptor
let descriptor = FetchDescriptor<Animal>(
    propertiesToFetch: [\.name, \.species]
)

// 在 SwiftUI 视图中使用
struct AnimalListView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var animals: [[String: Any]] = []

    var body: some View {
        List(animals, id: \.self) { animal in
            if let name = animal["name"] as? String,
               let species = animal["species"] as? String {
                Text("\(name) - \(species)")
            }
        }
        .onAppear {
            loadPartialData()
        }
    }

    private func loadPartialData() {
        do {
            let results = try modelContext.fetch(descriptor)
            animals = results
        } catch {
            print("Fetch failed: \(error)")
        }
    }
}

八、适用场景总结 #

场景是否推荐使用 propertiesToFetch
需要完整对象❌ 不推荐
仅显示部分字段✅ 推荐
大数据集统计✅ 推荐
频繁更新的高频查询✅ 推荐

通过合理使用 propertiesToFetch,可以在不牺牲功能的前提下显著提升应用性能,尤其在数据密集型的场景中效果更为明显。

本文共 2438 字,创建于 Feb 22, 2025
相关标签: Xcode, SwiftUI