FetchDescriptor 是苹果在 Swift Data 框架(WWDC23 推出)中引入的一个关键类型,用于定义数据查询的配置(过滤、排序、分页等)。它是 Swift Data 中替代 Core Data 的 NSFetchRequest 的现代化方案,完全基于 Swift 并发模型设计,支持类型安全和 Swift 原生语法。
一、核心功能#
| 功能 | 说明 |
|---|---|
| 数据过滤 | 通过 Predicate 宏定义查询条件(类型安全,支持 Swift 原生类型) |
| 数据排序 | 使用 SortDescriptor 定义排序规则(支持多字段排序) |
| 分页查询 | 通过 fetchLimit 和 offset 实现分页加载 |
| 关系预加载 | 通过 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.count3. 去重查询#
// 获取所有不重复的用户所在城市
let descriptor = FetchDescriptor<User>(
propertiesToFetch: [\.city],
returnsDistinctResults: true
)五、性能优化建议#
- 索引加速:对频繁查询的字段添加
@Attribute(.unique)或@Attribute(.indexed) - 批量加载:合理使用
fetchLimit+offset避免一次性加载过多数据 - 选择性加载:通过
propertiesToFetch仅加载必要字段 - 内存优化:设置
includesPropertyValues = false当仅需对象引用时 so
@Query#
在 SwiftData 中,@Query 属性包装器用于在 SwiftUI 视图中声明式地管理和自动更新数据查询。结合 FetchDescriptor,可以灵活配置查询条件、排序、分页等。以下是使用 @Query 的 init(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) // 手动更新查询
}
}
}注意事项#
- 自动更新:默认情况下,
@Query会监听数据上下文的变化并自动刷新视图。若需手动控制,可在FetchDescriptor中设置transactional: false。 - 性能优化:对大型数据集使用
fetchLimit和fetchOffset避免内存压力。 - 类型安全:
#Predicate会在编译时检查条件表达式,避免运行时错误。
通过结合 FetchDescriptor 的灵活配置和 SwiftUI 的动画系统,可以高效构建响应式数据驱动的界面。
propertiesToFetch#
在 SwiftData 中,FetchDescriptor 的 propertiesToFetch 属性用于优化数据查询性能,允许开发者指定仅获取实体(Entity)中的部分属性,而不是整个对象。这在处理大型数据集或仅需部分字段时非常有用。以下是具体用法和示例:
一、propertiesToFetch 的核心作用#
- 减少数据传输:仅从持久化存储中加载指定的属性,降低内存占用。
- 提升查询速度:减少数据库或存储引擎需要处理的数据量。
- 类型安全:通过 Swift 的 KeyPath 语法指定属性,避免字符串硬编码。
二、基本语法#
假设有一个 Animal 实体,包含 name、age 和 species 属性:
@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
}
}要仅获取 name 和 species 属性,可以按如下方式配置 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)")
}
}四、性能优化场景#
- 列表展示:在 UITableView 或 SwiftUI List 中仅需显示部分字段(如名称和物种)。
- 批量处理:在后台任务中统计或处理特定属性(如所有动物的平均年龄)。
- 网络同步:仅同步必要的字段到服务器,减少数据传输量。
五、对比传统 Core Data 的 NSFetchRequest#
在 Core Data 中,类似功能通过 NSFetchRequest 的 propertiesToFetch 实现:
let request = NSFetchRequest<NSDictionary>(entityName: "Animal")
request.resultType = .dictionaryResultType
request.propertiesToFetch = ["name", "species"]SwiftData 的优势:#
- 类型安全:使用 KeyPath(
\.name)而非字符串,避免拼写错误。 - Swift 原生集成:与 Swift 并发模型(
async/await)和 SwiftUI 更深度结合。
六、注意事项#
- 类型转换:返回的字典值需要手动转换为正确类型(如
as? String)。 - 不可访问未获取的属性:尝试访问未在
propertiesToFetch中指定的属性会导致运行时错误。 - 关联对象:如果获取的属性包含关联对象(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,可以在不牺牲功能的前提下显著提升应用性能,尤其在数据密集型的场景中效果更为明显。