PredicateKit 是 NSPredicate
的替代方案,它允许您使用 key-paths、比较运算符、逻辑运算符、字面量和函数,为 CoreData 编写具有表达力且类型安全的谓词。
CoreData 是一项强大的技术,但并非所有 API 都赶上了现代 Swift 世界的步伐。 具体来说,从 CoreData 获取和过滤对象严重依赖于 NSPredicate
和 NSExpression
。 遗憾的是,使用这些 API 很容易引入各种错误和运行时错误。 例如,我们可以将 String
类型的属性与 Int
类型的值进行比较,甚至在谓词中使用不存在的属性; 这些错误在编译时不会被注意到,但会在运行时导致重要的错误,这些错误可能不容易诊断。 这就是 PredicateKit 的用武之地,它可以几乎不可能引入这些类型的错误。
具体来说,PredicateKit 提供了
NSPredicate
的轻量级替代品,无需对代码库进行重大更改,无需遵循特殊的协议,无需配置等等。 只需 import PredicateKit
,编写您的谓词,并使用 NSManagedObjectContext.fetch(where:)
或 NSManagedObjectContext.count(where:)
函数来执行它们。将以下行添加到您的 Cartfile
。
github "ftchirou/PredicateKit" ~> 1.0.0
将以下行添加到您的 Podfile
。
pod 'PredicateKit', ~> '1.0.0'
更新您的 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(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()
有关修饰符的更多信息,请参见请求修饰符。
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)
}
}
}
您可以使用 updatePredicate
更新与 FetchedResults
关联的谓词。
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)
}
Button("Show recents") {
let recentDate: Date = // ...
notes.updatePredicate(\Note.createdAt >= recentDate)
}
}
}
这将导致关联的 FetchRequest
在点击 Show recents
按钮时使用新谓词执行获取。
PredicateKit 还扩展了 SwiftUI @SectionedFetchRequest
属性包装器以支持类型安全的谓词。
import PredicateKit
import SwiftUI
struct ContentView: View {
@SwiftUI.SectionedFetchRequest(
fetchRequest: FetchRequest(predicate: \User.name == "John Doe"),
sectionIdentifier: \.billingInfo.accountType
)
var users: SectionedFetchResults<String, User>
var body: some View {
List(users, id: \.id) { section in
Section(section.id) {
ForEach(section, id: \.objectID) { user in
Text(user.name)
}
}
}
}
您可以使用 updatePredicate
更新与 SectionedFetchedResults
关联的谓词。
import PredicateKit
import SwiftUI
struct ContentView: View {
@SwiftUI.SectionedFetchRequest(
fetchRequest: FetchRequest(predicate: \User.name == "John Doe"),
sectionIdentifier: \.billingInfo.accountType
)
var users: SectionedFetchResults<String, User>
var body: some View {
List(users, id: \.id) { section in
Section(section.id) {
ForEach(section, id: \.objectID) { user in
Text(user.name)
}
}
}
Button("Search") {
let query: String = // ...
users.updatePredicate((\User.name).contains(query))
}
}
}
这将导致关联的 FetchRequest
在点击 Search
按钮时使用新谓词执行获取。
在 UIKit 中,您可以使用 fetchedResultsController()
从已配置的获取请求创建 NSFetchedResultsController
。 fetchedResultsController
有两个可选参数
sectionNameKeyPath
是返回对象上的一个 key-path,用于计算section信息cacheName
是一个用于存储预先计算的section信息的文件名。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"))
谓词使用比较运算符、逻辑运算符、字面量和函数的组合来表示。
可以使用基本比较运算符 <
、<=
、==
、>=
和 >
之一来表达比较,其中运算符的左侧是 key-path,运算符的右侧是一个值,其类型与左侧 key-path 的值类型匹配。
class Note: NSManagedObject {
@NSManaged var text: String
@NSManaged var creationDate: Date
@NSManaged var numberOfViews: Int
@NSManaged var tags: [String]
@NSManaged var attachment: Attachment
@NSManaged var type: NoteType
}
class Attachment: NSManagedObject, Identifiable {
// ...
}
@objc enum NoteType: Int {
case freeForm
// ...
}
// 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
// Matches all free form notes (assuming `NoteType` is an enumeration whose `RawValue` conforms to `Equatable`).
let predicate = \Note.type == .freeForm
如果要比较的属性的类型为 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
函数来确定属性的值是否是参数可变列表、数组或集合中的值之一。
// 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 谓词匹配其两个操作数都匹配的对象。
// 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 谓词匹配其至少一个操作数匹配的对象。
// 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 谓词匹配其操作数不匹配的所有对象。
// 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"
// 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
如果您的对象与另一个对象具有一对一的关系,您只需使用适当的 key-path 即可定位该关系的任何属性。
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:)
函数创建。 第一个参数是要过滤的集合的 key-path,第二个参数是过滤集合的谓词。
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.creationDate) // Sort the matching objects by their creation date.
.result()
指定获取请求返回的对象数量。
managedObjectContext.fetch(where: ...)
.limit(50)
指定要跳过的初始匹配对象数量。
managedObjectContext.fetch(where: ...)
.offset(100)
指定获取请求中对象的批处理大小。
managedObjectContext.fetch(where: ...)
.batchSize(80)
指定要与获取请求的对象一起预取的关联关系的 key-paths。
managedObjectContext.fetch(where: ...)
.prefetchingRelationships(\.billingInfo, \.profiles)
relationshipKeyPathsForPrefetching
指定在托管对象上下文中未保存的更改是否包含在获取请求的结果中。
managedObjectContext.fetch(where: ...)
.includingPendingChanges(true)
指定执行获取请求时要搜索的持久存储。
let store1: NSPersistentStore = ...
let store2: NSPersistentStore = ...
managedObjectContext.fetch(where: ...)
.fromStores(store1, store2)
指定要获取的 key-paths。
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
指定获取请求是否仅返回 fetchingOnly(_:)
指定的 key-paths 的不同值。
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
.returningDistinctResults(true)
指定在请求结果类型为 [[String: Any]]
时,按哪些属性的 key-paths 对结果进行分组。
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
指定是否将获取对象的属性值更新为持久存储中的当前值。
managedObjectContext.fetch(where: ...)
.shouldRefreshRefetchedObjects(false)
指定要用于过滤具有 groupBy(_:)
修饰符应用的请求返回的对象的谓词。
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
.having((\Note.text).contains("Hello, World!"))
指定子实体是否包含在结果中。
managedObjectContext.fetch(where: ...)
.includingSubentities(true)
指定从获取请求返回的对象是否为 fault。
managedObjectContext.fetch(where: ...)
.returningObjectsAsFaults(true)
指定如何对请求返回的对象进行排序。此修饰符接受一个必需参数和两个可选参数。
by
:用于排序对象的键路径。(必需)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()
祝您编码愉快!⚡️