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.count
3. 去重查询 #
// 获取所有不重复的用户所在城市
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
,可以在不牺牲功能的前提下显著提升应用性能,尤其在数据密集型的场景中效果更为明显。