🎯 PredicateKit
PredicateKit 是 NSPredicate
的替代品,允许您使用 key-paths、比较运算符和逻辑运算符、字面量和函数,为 CoreData 编写表达式丰富且类型安全的谓词。
内容
动机
CoreData 是一项强大的技术,但其 API 并不完全跟上现代 Swift 世界的步伐。具体来说,从 CoreData 中检索和过滤对象主要依赖于 NSPredicate
和 NSExpression
。不幸的是,使用这些 API 容易引入一系列错误和运行时错误。例如,我们可以比较类型为 String
的属性和类型为 Int
的值,甚至在谓词中使用不存在的属性;这些错误在编译时可能不会被发现,但在运行时可能会引发重要的错误,这些错误可能难以诊断。这就是 PredicateKit 出来发挥作用,使引入这类错误变得几乎不可能。
具体来说,PredicateKit 提供
- 一个类型安全和表达式的 API 用于编写谓词。当使用 PredicateKit 时,你谓词中涉及的所有属性都使用 键路径 表达。这确保了在编译时就能捕捉到使用不存在的属性或打字错误。此外,所有比较、函数调用等操作都具有严格的类型,从而使得编写无效的谓词成为不可能。
- 改善了开发者体验。在编写谓词时,享受自动完成和语法高亮。此外,PredicateKit 只是对
NSPredicate
的轻量级替代,无需对你的代码库进行任何重大更改,无需遵守任何特殊协议,无需进行配置等。只需import PredicateKit
,编写你的谓词并使用NSManagedObjectContext.fetch(where:)
或NSManagedObjectContext.count(where:)
等功能来执行它们。
安装
Carthage
在你的 Cartfile
中添加以下行。
github "ftchirou/PredicateKit" ~> 1.0.0
CocoaPods
在你的 Podfile
中添加以下行。
pod 'PredicateKit', ~> '1.0.0'
Swift 包管理器
更新你的 Package.swift
文件中的 dependencies
数组。
dependencies: [
.package(url: "https://github.com/ftchirou/PredicateKit", .upToNextMajor(from: "1.0.0"))
]
快速入门
获取对象
使用 PredicateKit 获取对象时,请在 NSManagedObjectContext
的实例上调用 fetch(where:)
函数,并传递一个谓词作为参数。fetch(where:)
返回一个 FetchRequest
对象,您可以调用 result()
来执行请求并检索匹配的对象。
示例
let notes: [Note] = try managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.result()
您可以使用实体的键路径(key-paths),结合比较和逻辑运算符、字面值和函数调用来编写谓词。
关于编写谓词的更多信息,请参阅 编写谓词。
将对象作为字典获取
默认情况下,fetch(where:)
返回一个继承自NSManagedObject
的子类数组。您可以通过更改存储查询结果的变量类型,将返回的对象指定为字典数组([[String: Any]]
)。
示例
let notes: [[String: Any]] = try managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.result()
配置fetch
fetch(where:)
返回一个类型为FetchRequest
的对象。您可以通过在此对象上应用一系列修饰符来进一步配置对象匹配和返回方式。例如,sorted(by: \Note.creationDate, .descending)
是一个修饰符,指定对象应按创建日期降序排序。修饰符返回一个被修改的FetchRequest
;可以通过将一系列修饰符连接起来创建最终的FetchRequest
。
示例
let notes: [Note] = try managedObjectContext
.fetch(where: (\Note.text).contains("Hello, World!") && \Note.creationDate < Date())
.limit(50) // Return 50 objects matching the predicate.
.offset(100) // Skip the first 100 objects matching the predicate.
.sorted(by: \Note.creationDate) // Sort the matching objects by their creation date.
.result()
有关修饰符的更多信息,请参阅请求修饰符。
使用 @FetchRequest 属性包装器获取对象
PredicateKit 扩展了 SwiftUI 的 @FetchRequest
属性包装器以支持类型安全的谓词。要使用它,只需用一个谓词初始化一个 @FetchRequest
。
示例
import PredicateKit
import SwiftUI
struct ContentView: View {
@SwiftUI.FetchRequest(predicate: \Note.text == "Hello, World!")
var notes: FetchedResults<Note>
var body: some View {
List(notes, id: \.self) {
Text($0.text)
}
}
}
您还可以使用修改器和排序描述符以全功能的请求初始化 @FetchRequest
。
示例
import PredicateKit
import SwiftUI
struct ContentView: View {
@SwiftUI.FetchRequest(
fetchRequest: FetchRequest(predicate: (\Note.text).contains("Hello, World!"))
.limit(50)
.offset(100)
.sorted(by: \Note.creationDate)
)
var notes: FetchedResults<Note>
var body: some View {
List(notes, id: \.self) {
Text($0.text)
}
}
}
这两个初始化方法都接受一个可选参数 animation
,该参数将用于在获取到的结果中的变化进行动画处理。
示例
import PredicateKit
import SwiftUI
struct ContentView: View {
@SwiftUI.FetchRequest(
predicate: (\Note.text).contains("Hello, World!"),
animation: .easeInOut
)
var notes: FetchedResults<Note>
var body: some View {
List(notes, id: \.self) {
Text($0.text)
}
}
}
使用 NSFetchedResultsController 获取对象
在 UIKit 中,您可以使用 fetchedResultsController()
将配置好的获取请求创建为 NSFetchedResultsController
。《fetchedResultsController》有两个可选参数
sectionNameKeyPath
是用于计算章节信息的返回对象上的 key-pathcacheName
是存储预计算章节信息的文件的名称。
示例
let controller: NSFetchedResultsController<Note> = managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.sorted(by: \Note.creationDate, .descending)
.fetchedResultsController(sectionNameKeyPath: \Note.creationDate)
计数对象
为了计数匹配谓词的对象数量,使用 NSManagedObjectContext
实例的 count(where:)
函数。
示例
let count = try managedObjectContext.count(where: (\Note.text).beginsWith("Hello"))
文档
谓词编写
谓词通过比较运算符和逻辑运算符、字面值和函数的组合来表达。
比较
基本比较
比较可以使用基本比较运算符之一进行表达,包括 <
、<=
、==
、>=
和 >
,其中运算符的左侧是 键路径,运算符的右侧是与键路径左侧值类型匹配的值。
示例
class Note: NSManagedObject {
@NSManaged var text: String
@NSManaged var creationDate: Date
@NSManaged var numberOfViews: Int
@NSManaged var tags: [String]
@NSManaged var attachment: Attachment
}
// Matches all notes where the text is equal to "Hello, World!".
let predicate = \Note.text == "Hello, World!"
// Matches all notes created before the current date.
let predicate = \Note.creationDate < Date()
// Matches all notes where the number of views is at least 120.
let predicate = \Note.numberOfViews >= 120
// Matches all notes having the specified attachment. `Attachment` must conform to `Identifiable`.
let predicate = \Note.attachment == attachment
字符串比较
如果比较的属性类型是 String
,则可以使用特殊函数如 beginsWith
、contains
或 endsWith
进行比较。
// Matches all notes where the text begins with the string "Hello".
let predicate = (\Note.text).beginsWith("Hello")
// Matches all notes where the text contains the string "Hello".
let predicate = (\Note.text).contains("Hello")
// Matches all notes where the text matches the specified regular expression.
let predicate = (\Note.text).matches(NSRegularExpression(...))
以下任一函数均可用于字符串比较谓词。
beginsWith
contains
endsWith
like
matches
这些函数接受一个可选的第二个参数,以指定字符串比较应该如何执行。
// Case-insensitive comparison.
let predicate = (\Note.text).beginsWith("Hello, World!", .caseInsensitive)
// Diacritic-insensitive comparison.
let predicate = (\Note.text).beginsWith("Hello, World!", .diacriticInsensitive)
// Normalized comparison.
let predicate = (\Note.text).beginsWith("Hello, World!", .normalized)
成员检查
在...之间
您可以使用 between
函数或 ~=
运算符来判断属性的值是否位于指定的范围内。
// Matches all notes where the number of views is between 100 and 200.
let predicate = (\Note.numberOfViews).between(100...200)
// Or
let predicate = \Note.numberOfViews ~= 100...200
in
您可以使用 in
函数来判断属性的值是否为可变参数列表、数组或集中的一项。
// Matches all notes where the text is one of the elements in the specified variadic arguments list.
let predicate = (\Note.numberOfViews).in(100, 200, 300, 400)
// Matches all notes where the text is one of the elements in the specified array.
let predicate = (\Note.text).in([100, 200, 300, 400])
// Matches all notes where the text is one of the elements in the specified set.
let predicate = (\Note.text).in(Set([100, 200, 300, 400]))
当属性类型是 String
时,in
函数接受第二个参数,该参数决定了字符串应该如何与列表中的元素进行匹配。
// Case-insensitive comparison.
let predicate = (\Note.text).in(["a", "b", "c", "d"], .caseInsensitive)
复合谓词
复合谓词是可以逻辑组合一个、两个或更多谓词的谓词。
AND谓词
AND谓词使用&&
运算符,其中操作数是谓词。AND谓词匹配其两个操作数都匹配的对象。
// Matches all notes where the text begins with 'hello' and the number of views is at least 120.
let predicate = (\Note.text).beginsWith("hello") && \Note.numberOfViews >= 120
OR谓词
OR谓词使用||
运算符,其中操作数是谓词。OR谓词匹配至少有一个操作数匹配的对象。
// Matches all notes with the text containing 'hello' or created before the current date.
let predicate = (\Note.text).contains("hello") || \Note.creationDate < Date()
NOT谓词
NOT谓词使用一元!
运算符和一个谓词操作数。NOT谓词匹配其操作数不匹配的所有对象。
// Matches all notes where the text is not equal to 'Hello, World!'
let predicate = !(\Note.text == "Hello, World!")
数组操作
您可以对类型为Array
(或求值结果为类型Array
的表达式)的属性进行操作,并在谓词中使用结果。
在数组中选择一个元素
第一个
// Matches all notes where the first tag is 'To Do'..
let predicate = (\Note.tags).first == "To Do"
最后一个
// Matches all notes where the last tag is 'To Do'..
let predicate = (\Note.tags).last == "To Do"
在索引(index:)处选择
// Matches all notes where the third tag contains 'To Do'.
let predicate = (\Note.tags).at(index: 2).contains("To Do")
计算数组中元素的数量
计数
// Matches all notes where the number of elements in the `tags` array is less than 5.
let predicate = (\Note.tags).count < 5
// or
let predicate = (\Note.tags).size < 5
组合数组元素
如果数组的元素是数字,可以将它们组合或缩减成一个单独的数字,并在谓词中使用这个结果。
class Account: NSManagedObject {
@NSManaged var purchases: [Double]
}
求和
// Matches all accounts where the sum of the purchases is less than 2000.
let predicate = (\Account.purchases).sum < 2000
平均值
// Matches all accounts where the average purchase is 120.0
let predicate = (\Account.purchases).average == 120.0
最小值
// Matches all accounts where the minimum purchase is 98.5.
let predicate = (\Account.purchases).min == 98.5
最大值
// Matches all accounts where the maximum purchase is at least 110.5.
let predicate = (\Account.purchases).max >= 110.5
聚合比较
您还可以表达匹配数组所有、任意或没有元素的谓词。
全部
// Matches all accounts where every purchase is at least 95.0
let predicate = (\Account.purchases).all >= 95.0
任意
// Matches all accounts having at least one purchase of 20.0
let predicate = (\Account.purchases).any == 20.0
没有
// Matches all accounts where no purchase is less than 50.
let predicate = (\Account.purchases).none <= 50
具有一对一关系的谓词
如果你的对象与另一个对象具有一对一关系,你可以通过使用适当的关键路径来针对性地访问关系的任何属性。
示例
class User: NSManagedObject {
@NSManaged var name: String
@NSManaged var billingInfo: BillingInfo
}
class BillingInfo: NSManagedObject {
@NSManaged var accountType: String
@NSManaged var purchases: [Double]
}
// Matches all users with the billing account type 'Pro'
let predicate = \User.billingInfo.accountType == "Pro"
// Matches all users with an average purchase of 120
let predicate = (\User.billingInfo.purchases).average == 120.0
具有一对多关系的谓词
你可以使用 all(_:)
、any(_:)
或 none(_:)
函数在一系列关系中运行聚合操作。
示例
class Account: NSManagedObject {
@NSManaged var name: String
@NSManaged var profiles: Set<Profile>
}
class Profile: NSManagedObject {
@NSManaged var name: String
@NSManaged var creationDate: String
}
// Matches all accounts where all the profiles have the creation date equal to the specified one.
let predicate = (\Account.profiles).all(\.creationDate) == date
// Matches all accounts where any of the associated profiles has a name containing 'John'.
let predicate = (\Account.profiles).any(\.name).contains("John"))
// Matches all accounts where no profile has the name 'John Doe'
let predicate = (\Account.profiles).none(\.name) == "John Doe"
子谓词
当你的对象存在一对一关系时,你可以创建一个子谓词来过滤"多"关系,并在更复杂的谓词中使用子谓词的结果。子谓词是使用全局 all(_:where:)
函数创建的。第一个参数是过滤集合的键路径,第二个参数是用于过滤集合的谓词。
all(_:where:)
的结果是一个数组;这意味着你可以对它的结果执行任何有效的数组操作,例如 size
、first
等。
示例
// Matches all the accounts where the name contains 'Account' and where the number of profiles whose
// name contains 'Doe' is exactly 2.
let predicate = (\Account.name).contains("Account")
&& all(\.profiles, where: (\Profile.name).contains("Doe")).size == 2)
请求数据修饰符
通过将修饰符链应用到由 NSManagedObjectContext.fetch(where:)
返回的对象,可以配置如何返回匹配对象。
示例
let notes: [Note] = try managedObjectContext
.fetch(where: (\Note.text).contains("Hello, World!") && \Note.creationDate < Date())
.limit(50) // Return 50 objects matching the predicate.
.offset(100) // Skip the first 100 objects matching the predicate.
.sorted(by: \Note.text) // Sort the matching objects by their creation date.
.result()
限制
指定-fetch请求返回的对象数量。
使用
managedObjectContext.fetch(where: ...)
.limit(50)
NSFetchRequest
相当物
offset
指定要跳过的初始匹配对象的数量。
使用
managedObjectContext.fetch(where: ...)
.offset(100)
NSFetchRequest
相当物
batchSize
指定fetch请求中对象的批量大小。
使用方法
managedObjectContext.fetch(where: ...)
.batchSize(80)
NSFetchRequest
等价对象
预取关系
指定与fetch请求中的对象一起预取的关系的键值路径。
使用方法
managedObjectContext.fetch(where: ...)
.prefetchingRelationships(\.billingInfo, \.profiles)
NSFetchRequest
等价
包括未保存的更改
指定在执行查询请求时是否应将未保存的更改包含在查询请求的结果中。
用法
managedObjectContext.fetch(where: ...)
.includingPendingChanges(true)
NSFetchRequest
等价
来自存储
指定执行查询请求时要搜索的持久存储。
使用
let store1: NSPersistentStore = ...
let store2: NSPersistentStore = ...
managedObjectContext.fetch(where: ...)
.fromStores(store1, store2)
NSFetchRequest
等效
fetchingOnly
指定要提取的关键路径。
使用
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
NSFetchRequest
等效
返回唯一结果
指定fetch请求是否只返回由fetchingOnly(_:)
指定的key-paths指定的唯一值。
用法
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
.returningDistinctResults(true)
NSFetchRequest
等价项
groupBy
指定要按其分组结果的属性key-paths,当请求结果类型为[[String: Any]]
时。
用法
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
NSFetchRequest
等价
刷新重新获取的对象
指定是否应使用持久化存储中的当前值来更新获取的对象的属性值。
使用方法
managedObjectContext.fetch(where: ...)
.shouldRefreshRefetchedObjects(false)
NSFetchRequest
等价
条件
指定应用了groupBy(_:)
修饰符的请求返回的对象筛选条件。
使用
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
.having((\Note.text).contains("Hello, World!"))
NSFetchRequest
等效
includingSubentities
指定是否包含子实体在结果中。
使用
managedObjectContext.fetch(where: ...)
.includingSubentities(true)
NSFetchRequest
等效
返回错误对象
指定从获取请求返回的对象是否为错误。
使用
managedObjectContext.fetch(where: ...)
.returningObjectsAsFaults(true)
NSFetchRequest
等效方法
排序
指定请求返回的对象应该如何排序。此修改器接受一个必需参数和两个可选参数
by
:按照哪个key-path对对象进行排序。(必需)order
:对象的排序顺序。(可选,默认为.ascending
)comparator
:用于排序对象的自定义比较器。(可选,默认为nil
)
使用
managedObjectContext.fetch(where: ...)
.sorted(by: \.text)
.sorted(by: \.creationDate, .descending)
调试
在 DEBUG
模式下,您可以使用对 FetchRequest
使用修饰符 inspect(on:)
来检查正在执行的实际的 NSFetchRequest
。
示例
struct Inspector: NSFetchRequestInspector {
func inspect<Result>(_ request: NSFetchRequest<Result>) {
// Log or print the request here.
}
}
let notes: [Note] = try managedObjectContext
.fetch(where: \Note.text == "Hello, World!")
.sorted(by: \Note.creationDate, .descending)
.inspect(on: Inspector())
.result()
快乐编程!