https://developer.apple.com/documentation/foundation/predicate
在 SwiftData 中,Predicate(谓词)用于定义数据筛选条件,而 Sorting(排序)则用于指定数据检索后的顺序。SwiftData 基于 Swift 宏(如 #Predicate)和类型安全的设计,提供了更直观的语法来构建查询。以下是详细说明及示例:
一、Predicate 基础
SwiftData 的 #Predicate 宏允许用 Swift 语法直接编写类型安全的查询条件,编译器会检查语法和类型错误。
基本语法结构:
let predicate = #Predicate<T> { (object: T) in
// 条件表达式
}
二、Filter 场景示例
假设有一个 TodoItem 数据模型:
@Model
class TodoItem {
var title: String
var dueDate: Date
var isCompleted: Bool
var priority: Int
init(title: String, dueDate: Date, isCompleted: Bool, priority: Int) {
self.title = title
self.dueDate = dueDate
self.isCompleted = isCompleted
self.priority = Int(priority)
}
}
1. 基本比较
// 筛选未完成的任务
let incompletePredicate = #Predicate<TodoItem> { $0.isCompleted == false }
// 优先级大于等于3的任务
let highPriorityPredicate = #Predicate<TodoItem> { $0.priority >= 3 }
2. 字符串操作
// 标题包含 "urgent" 的任务(不区分大小写)
let containsPredicate = #Predicate<TodoItem> {
$0.title.localizedStandardContains("urgent")
}
// 标题以 "Fix" 开头的任务
let beginsWithPredicate = #Predicate<TodoItem> {
$0.title.localizedStandardStartsWith("Fix")
}
3. 日期比较
// 截止日期在今天之后的任务
let futureDuePredicate = #Predicate<TodoItem> {
$0.dueDate > Date.now
}
// 截止日期在本月内的任务
let calendar = Calendar.current
let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: .now))!
let endOfMonth = calendar.date(byAdding: DateComponents(month: 1, day: -1), to: startOfMonth)!
let thisMonthPredicate = #Predicate<TodoItem> {
$0.dueDate >= startOfMonth && $0.dueDate <= endOfMonth
}
4. 复合条件(AND/OR)
// 高优先级(>=3)且未完成的任务
let highPriorityIncompletePredicate = #Predicate<TodoItem> {
$0.priority >= 3 && $0.isCompleted == false
}
// 标题包含 "bug" 或优先级为5的任务
let bugOrCriticalPredicate = #Predicate<TodoItem> {
$0.title.localizedStandardContains("bug") || $0.priority == 5
}
5. 集合操作
// 优先级在指定数组中的任务
let allowedPriorities = [1, 3, 5]
let inPredicate = #Predicate<TodoItem> {
allowedPriorities.contains($0.priority)
}
三、Sorting 场景示例
使用 SortDescriptor 指定排序规则,支持多个排序条件。
1. 单字段排序
// 按截止日期升序排列
let dueDateSort = SortDescriptor<TodoItem>(\.dueDate, order: .forward)
// 按优先级降序排列
let prioritySort = SortDescriptor<TodoItem>(\.priority, order: .reverse)
2. 多字段排序
// 先按是否完成排序(未完成在前),再按截止日期升序
let multiSort = [
SortDescriptor<TodoItem>(\.isCompleted, order: .forward),
SortDescriptor<TodoItem>(\.dueDate)
]
四、结合 FetchDescriptor 使用
将 Predicate 和 Sorting 应用到数据查询:
import SwiftData
// 创建 FetchDescriptor
let descriptor = FetchDescriptor<TodoItem>(
predicate: highPriorityIncompletePredicate, // 筛选条件
sortBy: [prioritySort, dueDateSort] // 排序规则
)
// 执行查询
let context = ModelContext(container: modelContainer)
let todos = try? context.fetch(descriptor)
五、高级用法示例
1. 关系型查询(假设 TodoItem 有 category 属性)
@Model
class Category {
var name: String
var todos: [TodoItem]
init(name: String) {
self.name = name
self.todos = []
}
}
// 筛选属于 "Work" 分类的任务
let workCategoryPredicate = #Predicate<TodoItem> {
$0.category?.name == "Work"
}
2. 使用 Optional 安全解包
// 筛选有分类且分类名不为空的任务
let validCategoryPredicate = #Predicate<TodoItem> {
$0.category != nil && $0.category!.name.isEmpty == false
}
3. 自定义函数扩展
// 扩展 Date 判断是否为今天
extension Date {
var isToday: Bool {
Calendar.current.isDateInToday(self)
}
}
// 筛选截止日期为今天的任务
let dueTodayPredicate = #Predicate<TodoItem> {
$0.dueDate.isToday
}
六、注意事项
- 类型安全:
#Predicate会在编译时检查属性是否存在及类型是否匹配。 - 性能:避免在谓词中执行复杂计算,优先使用索引优化。
- 区分大小写:
localizedStandardContains提供不区分大小写的查询,而contains是区分大小写的。 - 空值处理:使用
Optional类型时需显式解包(如$0.category?.name == "Work")。
通过灵活组合 Predicate 和 SortDescriptor,可以高效实现复杂的数据查询需求。
报错 1
Cannot convert value of type ‘PredicateExpressions.CollectionContainsCollection<PredicateExpressions.KeyPath<PredicateExpressions.Variable
, String>, PredicateExpressions.KeyPath<PredicateExpressions.Value , String>>’ to closure result type ‘any StandardPredicateExpression ’
let descriptor = #Predicate<UserTodo> { item in
item.title.contains(todo.title)
}
这里出现错误的原因是在 SwiftData 的 #Predicate 宏中,使用 contains 方法时出现了类型不匹配问题。错误信息表明:不能将 PredicateExpressions.CollectionContainsCollection 类型转换为 any StandardPredicateExpression<Bool> 类型。
简单来说,在 SwiftData 的谓词表达式中,当你使用 contains 方法时,不能直接将两个不同上下文的变量进行比较。item.title 是一个谓词表达式变量,而 todo.title 是一个已经确定的值,这两种类型不能直接进行 contains 操作。
当在 #Predicate 中使用 contains 方法时,右侧应该是一个字面值或常量,而不是另一个模型对象的属性。修复方案:
let title = todo.title
let descriptor = #Predicate<UserTodo> { item in
item.title.contains(title)
}