在 SwiftData 中,Predicate 是一个强类型、声明式的过滤条件工具,用于从数据模型中筛选符合条件的数据。它提供了更安全、更直观的查询方式,与 Swift 的类型系统紧密结合。
核心概念
1、定义方式:
使用 #Predicate 表示,这是一种编译时检查的过滤条件构造器,确保在运行时之前验证语法和类型正确性。
2、与 SwiftData 集成:
它通常与 SwiftData 的查询机制(如 @Query)一起使用,用来筛选持久化存储中的数据。
3、类型限制:
Predicate 是泛型类型,指定用于某种数据模型(如 Predicate<Place>)。
使用 Predicate 的示例
1、简单筛选条件
let predicate = #Predicate<Place> {
$0.name.localizedStandardContains("park")
}
上例筛选出 name 属性中包含 “park” 的 Place 对象。
2、多条件筛选
let predicate = #Predicate<Place> {
$0.name.localizedStandardContains("park") && $0.interested
}
同时满足以下条件的数据会被选中:
1、name 包含 “park”。
2、interested 属性为 true。
3、动态筛选条件
func createPredicate(searchText: String, filterInterested: Bool) -> Predicate<Place> {
#Predicate<Place> {
if !searchText.isEmpty && filterInterested {
$0.name.localizedStandardContains(searchText) && $0.interested
} else if !searchText.isEmpty {
$0.name.localizedStandardContains(searchText)
} else if filterInterested {
$0.interested
} else {
true
}
}
}
根据 searchText 和 filterInterested 动态生成不同的筛选条件。
Predicate 的构造方式
1、固定属性引用:
使用 $0 表示当前查询的对象实例。它的属性需要是模型定义中的持久化属性。
2、逻辑操作:
&&:表示逻辑 “与”。
||:表示逻辑 “或”。
!:表示逻辑 “非”。
3、比较运算:
支持标准比较运算符(如 ==, !=, <, > 等)。
示例:$0.age > 30 筛选年龄大于 30 的对象。
4、字符串操作:
localizedStandardContains: 检查子字符串的存在(区分大小写)。
hasPrefix, hasSuffix: 检查字符串前缀或后缀。
示例:$0.name.hasPrefix(“John”) 筛选名字以 “John” 开头的数据。
5、动态逻辑:
Predicate 支持 if 条件和变量插值,但不支持在表达式中直接调用复杂方法。
与 @Query 结合使用
在 SwiftUI 中,Predicate 通常与 @Query 属性包装器结合,用来查询持久化存储的数据:
@Query(filter: #Predicate<Place> { $0.name.starts("be") })
private var places: [Place]
动态更新筛选条件:
@State private var searchText = ""
@Query var places: [Place]
init() {
_places = Query(filter: #Predicate<Place> {
$0.name.localizedStandardContains(searchText)
})
}
与搜索功能结合使用
在SwiftUI视图中实现搜索功能时,也可以通过#Predicate进行筛选。
import SwiftUI
import SwiftData
struct PlaceList: View {
@Query(sort:\Place.name) private var places: [Place]
@State private var showImages = false
@State private var searchText = ""
@State private var filterByInterested = false
var body: some View {
NavigationStack {
List(places) { place in
HStack {
place.image
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: 7))
.frame(width: 100, height: 100)
Text(place.name)
Spacer()
if place.interested {
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
.padding(.trailing)
}
}
}
.navigationTitle("Places")
.searchable(text: $searchText ,prompt: "Find a Place")
.animation(.default, value:searchText)
.toolbar {
ToolbarItem(placement:.topBarLeading) {
Button("Filter", systemImage: filterByInterested ? "star.fill" : "star") {
withAnimation {
filterByInterested.toggle()
}
}
.tint(filterByInterested ? .yellow : .blue)
}
ToolbarItem(placement:.topBarTrailing) {
Button("Show Image", systemImage: "photo") {
showImages.toggle()
}
}
}
.sheet(isPresented: $showImages, content: {
Scrolling()
})
}
}
}
在这段代码中,基本的功能为一个List列表。通过searchable实现一个信息检索的功能。这里的searchable就可以通过#Predicate实现。
首先声明一个#Predicate<Place>类型的变量:
private var predicate: Predicate<Place> {
#Predicate<Place> {
if !searchText.isEmpty && filterByInterested == true {
$0.name.localizedStandardContains(searchText) && $0.interested
} else if !searchText.isEmpty {
$0.name.localizedStandardContains(searchText)
} else if filterByInterested == true {
$0.interested
} else {
true
}
}
}
代码解析
1、计算属性的意义
private var predicate: Predicate<Place> { ... }
动态生成: 每次访问 predicate 时,都会根据当前的 searchText 和 filterByInterested 的值重新生成筛选条件。
简洁复用:通过集中定义 predicate,可以避免重复代码,将复杂的筛选逻辑封装成一个易维护的单元。
2、筛选逻辑
#Predicate<Place> { ... }
#Predicate 是 SwiftData 提供的一种类型安全的查询工具,可以用类似 Swift 的逻辑语法构建复杂的筛选条件。
作用:定义模型 Place 的筛选逻辑。
类型约束:这里的泛型 <Place> 表明此谓词仅适用于 Place 数据模型。
编译时检查:#Predicate 会在编译时验证筛选条件的语法和类型是否正确。
调用#Predicate
在代码中,通过places.filter(predicate)表明希望动态过滤本地数据集(places),并不是直接通过 @Query 从 SwiftData 数据存储中查询。
List((try? places.filter(predicate)) ?? places) { place in ...}
1、本地数据过滤:
places 是一个已加载的数据数组,filter(predicate) 是对这个数组执行的操作。
它使用的是 Swift 的标准数组方法 filter,而不是 SwiftData 内置的查询功能。
2、try? 包裹:
如果 filter 方法抛出异常(如 predicate 无效),它会返回原始数据 places,避免崩溃。
3、差异原因:
这里的 predicate 是从计算属性中生成的,而不是直接绑定到 @Query 的筛选条件中。
实现效果
对比searchable修饰符文章中的筛选条件,使用#Predicate可能看起来比较复杂,但因为#Predicate同样支持条件筛选,因此单独拿出来讲一下。
优势
1、类型安全:
Predicate 在编译时检查语法和类型,避免运行时错误。
2、简洁声明式:
使用 Swift 的语法直接表达筛选条件,避免复杂的字符串拼接查询。
3、优化性能:
查询条件被直接传递到底层存储引擎执行,减少数据加载和过滤的开销。
注意事项
1、受限的动态性:
Predicate 是编译时检查的,因此不支持所有动态计算逻辑(如动态函数调用)。
2、性能:
确保筛选逻辑简洁高效,因为复杂的筛选条件可能影响查询性能。
3、SwiftData 版本要求:
确保使用支持 #Predicate 的 SwiftData 和 Swift 版本。
Predicate 是 SwiftData 的一个强大特性,结合类型安全和声明式编程理念,适合在数据驱动的 SwiftUI 应用中高效实现筛选和查询功能。
参考文章
1、SwiftData框架属性包装器@Query:https://fangjunyu.com/2024/11/04/swiftdata%e6%a1%86%e6%9e%b6%e5%b1%9e%e6%80%a7%e5%8c%85%e8%a3%85%e5%99%a8query/
2、iOS 18, SwiftUI 6, & Swift 6: 从零开始构建iOS应用程序, 涵盖visionOS, macOS, watchOS:https://www.bilibili.com/video/BV1b6421f7Px?spm_id_from=333.788.videopod.episodes&vd_source=f21219cb93118beac6a36b0ef961df6a&p=8
3、SwiftUI视图内实现搜索功能的searchable修饰符:https://fangjunyu.com/2024/12/23/swiftui%e8%a7%86%e5%9b%be%e5%86%85%e5%ae%9e%e7%8e%b0%e6%90%9c%e7%b4%a2%e5%8a%9f%e8%83%bd%e7%9a%84searchable%e4%bf%ae%e9%a5%b0%e7%ac%a6/
扩展部分
与搜素功能结合的完整代码
import SwiftUI
import SwiftData
struct PlaceList: View {
@Query(sort:\Place.name) private var places: [Place]
@State private var showImages = false
@State private var searchText = ""
@State private var filterByInterested = false
private var predicate: Predicate<Place> {
#Predicate<Place> {
if !searchText.isEmpty && filterByInterested == true {
$0.name.localizedStandardContains(searchText) && $0.interested
} else if !searchText.isEmpty {
$0.name.localizedStandardContains(searchText)
} else if filterByInterested == true {
$0.interested
} else {
true
}
}
}
var body: some View {
NavigationStack {
List((try? places.filter(predicate)) ?? places) { place in
HStack {
place.image
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: 7))
.frame(width: 100, height: 100)
Text(place.name)
Spacer()
if place.interested {
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
.padding(.trailing)
}
}
}
.navigationTitle("Places")
.searchable(text: $searchText ,prompt: "Find a Place")
.animation(.default, value:searchText)
.toolbar {
ToolbarItem(placement:.topBarLeading) {
Button("Filter", systemImage: filterByInterested ? "star.fill" : "star") {
withAnimation {
filterByInterested.toggle()
}
}
.tint(filterByInterested ? .yellow : .blue)
}
ToolbarItem(placement:.topBarTrailing) {
Button("Show Image", systemImage: "photo") {
showImages.toggle()
}
}
}
.sheet(isPresented: $showImages, content: {
Scrolling()
})
}
}
}
#Preview {
PlaceList()
.modelContainer(Place.preview)
}