PredicateKit is an alternative to NSPredicate
allowing you to
write expressive and type-safe predicates for CoreData using key-paths,
comparisons and logical operators, literal values, and functions.
CoreData is a formidable piece of technology, however not all of its API has caught up with the modern Swift world. Specifically, fetching and filtering objects from
CoreData relies heavily on NSPredicate
and NSExpression
. Unfortunately, a whole range of bugs and runtime errors can easily be introduced using those APIs.
For instance, we can compare a property of type String
to a value of type Int
or even use a non-existant property in a predicate; these mistakes will go un-noticed
at compile time but can cause important errors at runtime that may not be obvious to diagnose. This is where PredicateKit comes in by making it virtually impossible to
introduce these types of errors.
Concretely, PredicateKit provides
- a type-safe and expressive API for writing predicates. When using PredicateKit, all properties involved in your predicates are expressed using key-paths. This ensures that the usage of inexistant properties or typos are caught at compile time. Additionally, all operations such as comparisons, functions calls, etc. are strongly-typed, making it impossible to write invalid predicates.
- an improved developer experience. Enjoy auto-completion and syntax highlighting when writing your predicates. In addition, PredicateKit
is just a lightweight replacement for
NSPredicate
, no major change to your codebase is required, no special protocol to conform to, no configuration, etc. Simplyimport PredicateKit
, write your predicates and use the functionsNSManagedObjectContext.fetch(where:)
orNSManagedObjectContext.count(where:)
to execute them.
Add the following line to your Cartfile
.
github "ftchirou/PredicateKit" ~> 1.0.0
Add the following line to your Podfile
.
pod 'PredicateKit', ~> '1.0.0'
Update the dependencies
array in your Package.swift
.
dependencies: [
.package(url: "https://github.com/ftchirou/PredicateKit", .upToNextMajor(from: "1.0.0"))
]
To fetch objects using PredicateKit, use the function fetch(where:)
on an instance of NSManagedObjectContext
passing as argument a predicate. fetch(where:)
returns an object of type FetchRequest
on which you call result()
to execute the request and retrieve the matching objects.
let notes: [Note] = try managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.result()
You write your predicates using the key-paths of the entity to filter and a combination of comparison and logical operators, literal values, and functions calls.
See Writing predicates for more about writing predicates.
By default, fetch(where:)
returns an array of subclasses of NSManagedObject
. You can specify that the objects be returned as an array of dictionaries ([[String: Any]]
)
simply by changing the type of the variable storing the result of the fetch.
let notes: [[String: Any]] = try managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.result()
fetch(where:)
returns an object of type FetchRequest
. You can apply a series of modifiers on this object to further configure how the objects should be matched and returned.
For example, sorted(by: \Note.creationDate, .descending)
is a modifier specifying that the objects should be sorted by the creation date in the descending order. A modifier returns a mutated FetchRequest
; a series
of modifiers can be chained together to create the final 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()
See Request modifiers for more about modifiers.
PredicateKit extends the SwiftUI @FetchRequest
property wrapper to support type-safe predicates. To use, simply initialize a @FetchRequest
with a predicate.
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)
}
}
}
You can also initialize a @FetchRequest
with a full-fledged request with modifiers and sort descriptors.
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)
}
}
}
Both initializers accept an optional parameter animation
that will be used to animate changes in the fetched results.
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)
}
}
}
In UIKit, you can use fetchedResultsController()
to create an NSFetchedResultsController
from a configured fetch request. fetchedResultsController
has two optional parameters:
sectionNameKeyPath
is a key-path on the returned objects used to compute section infocacheName
is the name of a file to store pre-computed section info.
let controller: NSFetchedResultsController<Note> = managedObjectContext
.fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date())
.sorted(by: \Note.creationDate, .descending)
.fetchedResultsController(sectionNameKeyPath: \Note.creationDate)
To count the number of objects matching a predicate, use the function count(where:)
on an instance of NSManagedObjectContext
.
let count = try managedObjectContext.count(where: (\Note.text).beginsWith("Hello"))
Predicates are expressed using a combination of comparison operators and logical operators, literal values, and functions.
A comparison can be expressed using one of the basic comparison operators <
, <=
, ==
, >=
, and >
where the left hand side of
the operator is a key-path and the right hand side
of the operator is a value whose type matches the value type of the key-path on the left hand side.
class Note: NSManagedObject {
@NSManaged var text: String
@NSManaged var creationDate: Date
@NSManaged var numberOfViews: Int
@NSManaged var tags: [String]
}
// 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
If the property to compare is of type String
, comparisons can be additionally expressed with special functions such as beginsWith
,
contains
, or 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(...))
Any of the following functions can be used in a string comparison predicate.
beginsWith
contains
endsWith
like
matches
These functions accept a second optional parameter specifying how the string comparison should be performed.
// 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)
You can use the between
function or the ~=
operator to determine whether a property's value is within a specified range.
// 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
You can use the in
function to determine whether a property's value is one of the values in a specified list.
// Matches all notes where the text is one of the elements in the specified list.
let predicate = (\Note.text).in("a", "b", "c", "d")
When the property is of type String
, in
accepts a second parameter that determines how the string should be compared to the elements in the list.
// Case-insensitive comparison.
let predicate = (\Note.text).in(["a", "b", "c", "d"], .caseInsensitive)
Compound predicates are predicates that logically combine one, two or more predicates.
AND predicates are expressed with the &&
operator where the operands are predicates. An AND predicate
matches objects where both its operands match.
// 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 predicates are expressed with the ||
operator where the operands are predicates. An OR predicate matches
objects where at least one of its operands matches.
// Matches all notes with the text containing 'hello' or created before the current date.
let predicate = (\Note.text).contains("hello") || \Note.creationDate < Date()
NOT predicates are expressed with the unary !
operator with a predicate operand. A NOT predicate matches all objects
where its operand does not match.
// Matches all notes where the text is not equal to 'Hello, World!'
let predicate = !(\Note.text == "Hello, World!")
You can perform operations on properties of type Array
(or expressions that evaluate to values of type Array
) and use the result in a predicate.
// 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
If the elements of an array are numbers, you can combine or reduce them into a single number and use the result in a predicate.
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
You can also express predicates matching all, any, or none of the elements of an array.
// 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
If your object has a one-to-one relationship with another one, you can target any property of the relationship simply by using the appropriate 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
You can run aggregate operations on a set of relationships using the all(_:)
, any(_:)
, or none(_:)
functions.
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.profile).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"
When your object has one-to-many relationships, you can create a sub-predicate that filters the "many" relationships and use the
resuult of the sub-predicate in a more complex predicate. Sub-predicates are created using the global all(_:where:)
function. The first
parameter is the key-path of the collection to filter and the second parameter is a predicate that filters the collection.
all(_:where:)
evaluates to an array; that means you can perform any valid array operation on its result such as size
, first
, etc.
// 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)
You can configure how matching objects are returned by applying a chain of modifiers to the object returned by 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()
Specifies the number of objects returned by the fetch request.
managedObjectContext.fetch(where: ...)
.limit(50)
Specifies the number of initial matching objects to skip.
managedObjectContext.fetch(where: ...)
.offset(100)
Specifies the batch size of the objects in the fetch request.
managedObjectContext.fetch(where: ...)
.batchSize(80)
Specifies the key-paths of the relationships to prefetch along with objects of the fetch request.
managedObjectContext.fetch(where: ...)
.prefetchingRelationships(\.billingInfo, \.profiles)
relationshipKeyPathsForPrefetching
Specifies whether changes unsaved in the managed object context are included in the result of the fetch request.
managedObjectContext.fetch(where: ...)
.includingPendingChanges(true)
Specifies the persistent stores to be searched when the fetch request is executed.
let store1: NSPersistentStore = ...
let store2: NSPersistenStore = ...
managedObjectContext.fetch(where: ...)
.fromStores(store1, store2)
Specifies the key-paths to fetch.
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
Specifies whether the fetch request returns only distinct values for the key-paths specified by fetchingOnly(_:)
.
managedObjectContext.fetch(where: ...)
.fetchingOnly(\.text, \.creationDate)
.returningDistinctResults(true)
Specifies the key-paths of the properties to group the result by, when the result of the request is of type [[String: Any]]
.
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
Specifies whether the property values of fetched objects will be updated with the current values in the persistent store.
managedObjectContext.fetch(where: ...)
.shouldRefreshRefetchedObjects(false)
pecifies the predicate to use to filter objects returned by a request with a groupBy(_:)
modifier applied.
let result: [[String: Any]] = managedObjectContext.fetch(where: ...)
.groupBy(\.creationDate)
.having((\Note.text).contains("Hello, World!"))
Specifies whether subentities are included in the result.
managedObjectContext.fetch(where: ...)
.includingSubentities(true)
Specifies whether objects returned from the fetch request are faults.
managedObjectContext.fetch(where: ...)
.returningObjectsAsFaults(true)
Specifies how the objects returned by the request should be sorted. This modifier takes one required parameter and 2 optional ones:
by
: the key-path by which to sort the objects. (Required)order
: the order in which to sort the object. (Optional, defaults to.ascending
)comparator
: a custom comparator to use to sort the objects. (Optional, defaults tonil
)
managedObjectContext.fetch(where: ...)
.sorted(by: \.text)
.sorted(by: \.creationDate, .descending)
In DEBUG
mode, you can inspect the actual NSFetchRequest
s that are being executed by using the modifier inspect(on:)
on a FetchRequest
.
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()
Happy coding! β‘οΈ