🎯 PredicateKit

GitHub Workflow Status (branch) GitHub release (latest SemVer)

PredicateKitNSPredicate 的替代方案,它允许您使用 key-paths、比较运算符、逻辑运算符、字面量和函数,为 CoreData 编写具有表达力且类型安全的谓词。

目录

动机

CoreData 是一项强大的技术,但并非所有 API 都赶上了现代 Swift 世界的步伐。 具体来说,从 CoreData 获取和过滤对象严重依赖于 NSPredicateNSExpression。 遗憾的是,使用这些 API 很容易引入各种错误和运行时错误。 例如,我们可以将 String 类型的属性与 Int 类型的值进行比较,甚至在谓词中使用不存在的属性; 这些错误在编译时不会被注意到,但会在运行时导致重要的错误,这些错误可能不容易诊断。 这就是 PredicateKit 的用武之地,它可以几乎不可能引入这些类型的错误。

具体来说,PredicateKit 提供了

安装

Carthage

将以下行添加到您的 Cartfile

github "ftchirou/PredicateKit" ~> 1.0.0

CocoaPods

将以下行添加到您的 Podfile

pod 'PredicateKit', ~> '1.0.0'

Swift Package Manager

更新您的 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()

有关修饰符的更多信息,请参见请求修饰符

使用 @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)
    }
  }
}

您可以使用 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 按钮时使用新谓词执行获取。

使用 @SectionedFetchRequest 属性包装器获取对象

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 按钮时使用新谓词执行获取。

使用 NSFetchedResultsController 获取对象

在 UIKit 中,您可以使用 fetchedResultsController() 从已配置的获取请求创建 NSFetchedResultsControllerfetchedResultsController 有两个可选参数

示例
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,则可以使用诸如 beginsWithcontainsendsWith 之类的特殊函数来额外表达比较。

// 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(...))

以下任何函数都可以在字符串比较谓词中使用。

这些函数接受第二个可选参数,用于指定应如何执行字符串比较。

// 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

您可以使用 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 类型值的表达式)执行操作,并在谓词中使用结果。

选择数组中的一个元素

first
// Matches all notes where the first tag is 'To Do'..
let predicate = (\Note.tags).first == "To Do"
last
// Matches all notes where the last tag is 'To Do'..
let predicate = (\Note.tags).last == "To Do"
at(index:)
// Matches all notes where the third tag contains 'To Do'.
let predicate = (\Note.tags).at(index: 2).contains("To Do")

计算数组中元素的数量

count
// 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]
}
sum
// Matches all accounts where the sum of the purchases is less than 2000.
let predicate = (\Account.purchases).sum < 2000
average
// Matches all accounts where the average purchase is 120.0
let predicate = (\Account.purchases).average == 120.0
min
// Matches all accounts where the minimum purchase is 98.5.
let predicate = (\Account.purchases).min == 98.5
max
// Matches all accounts where the maximum purchase is at least 110.5.
let predicate = (\Account.purchases).max >= 110.5

聚合比较

您还可以表达匹配数组所有、任何或全部元素的谓词。

all
// Matches all accounts where every purchase is at least 95.0
let predicate = (\Account.purchases).all >= 95.0
any
// Matches all accounts having at least one purchase of 20.0
let predicate = (\Account.purchases).any == 20.0
none
// 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:) 的计算结果为数组; 这意味着您可以对其结果执行任何有效的 数组操作,例如 sizefirst 等。

示例
// 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()

limit

指定获取请求返回的对象数量。

用法
managedObjectContext.fetch(where: ...)
  .limit(50)
NSFetchRequest 等效项

fetchLimit

offset

指定要跳过的初始匹配对象数量。

用法
managedObjectContext.fetch(where: ...)
  .offset(100)
NSFetchRequest 等效项

fetchOffset

batchSize

指定获取请求中对象的批处理大小。

用法
managedObjectContext.fetch(where: ...)
  .batchSize(80)
NSFetchRequest 等效项

fetchBatchSize

prefetchingRelationships

指定要与获取请求的对象一起预取的关联关系的 key-paths。

用法
managedObjectContext.fetch(where: ...)
  .prefetchingRelationships(\.billingInfo, \.profiles)
NSFetchRequest 等效项

relationshipKeyPathsForPrefetching

includingPendingChanges

指定在托管对象上下文中未保存的更改是否包含在获取请求的结果中。

用法
managedObjectContext.fetch(where: ...)
  .includingPendingChanges(true)
NSFetchRequest 等效项

includesPendingChanges

fromStores

指定执行获取请求时要搜索的持久存储。

用法
let store1: NSPersistentStore = ...
let store2: NSPersistentStore = ...

managedObjectContext.fetch(where: ...)
  .fromStores(store1, store2)
NSFetchRequest 等效项

affectedStores

fetchingOnly

指定要获取的 key-paths。

用法
managedObjectContext.fetch(where: ...)
  .fetchingOnly(\.text, \.creationDate)
NSFetchRequest 等效项

propertiesToFetch

returningDistinctResults

指定获取请求是否仅返回 fetchingOnly(_:) 指定的 key-paths 的不同值。

用法
managedObjectContext.fetch(where: ...)
  .fetchingOnly(\.text, \.creationDate)
  .returningDistinctResults(true)
NSFetchRequest 等效项

returnsDistinctResults

groupBy

指定在请求结果类型为 [[String: Any]] 时,按哪些属性的 key-paths 对结果进行分组。

用法
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
  .groupBy(\.creationDate)
NSFetchRequest 等效项

propertiesToGroupBy

refreshingRefetchedObjects

指定是否将获取对象的属性值更新为持久存储中的当前值。

用法
managedObjectContext.fetch(where: ...)
  .shouldRefreshRefetchedObjects(false)
NSFetchRequest 等效项

shouldRefreshRefetchedObjects

having

指定要用于过滤具有 groupBy(_:) 修饰符应用的请求返回的对象的谓词。

用法
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
  .groupBy(\.creationDate)
  .having((\Note.text).contains("Hello, World!"))
NSFetchRequest 等效项

havingPredicate

includingSubentities

指定子实体是否包含在结果中。

用法
managedObjectContext.fetch(where: ...)
  .includingSubentities(true)
NSFetchRequest 等效项

includesSubentities

returningObjectsAsFaults

指定从获取请求返回的对象是否为 fault。

用法
managedObjectContext.fetch(where: ...)
  .returningObjectsAsFaults(true)
NSFetchRequest 等效项

returnsObjectsAsFaults

sorted

指定如何对请求返回的对象进行排序。此修饰符接受一个必需参数和两个可选参数。

用法
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()

祝您编码愉快!⚡️