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)
}