Reputation: 3439
I'm trying to combine multiple Predicates of the Type with and / or. Previously with CoreData and NSPredicate I'd just do this:
let predicate = NSPredicate(value: true)
let predicate2 = NSPredicate(value: false)
let combinedPred = NSCompoundPredicate(type: .or, subpredicates: [predicate, predicate2])
Is there a comparable way to do this using SwiftData and #Predicate? And if not, how could I implement a way to create partial conditions beforehand and combine them in a predicate later?
The only way I've found of doing this as an expression is like this, but this would make my predicate hundredths of lines long
let includeOnlyFavorites = true
#Predicate { includeOnlyFavorites ? $0.isFavorite : true }
I'm developing an App that allows users to save and query items using shortcut actions. The items are stored using SwiftData and queried using EntityPropertyQuery
Apple implements the Query properties like this:
static var properties = QueryProperties {
Property(\BookEntity.$title) {
EqualToComparator { NSPredicate(format: "title = %@", $0) }
ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
}
}
and later combines the predicates with NSCompoundPredicate
.
Closure with Bool return:
let isFavorite = { (item: Item) in item.isFavorite }
let predicate = #Predicate<Item> { isFavorite($0) }
evaluate(Item) -> Bool
method but I also can't use thatI also thought i might be able to use StandardPredicateExpression in another predicate because in the documentation it reads:
"A component expression that makes up part of a predicate, and that's supported by the standard predicate type." but there are no further explanations on this type
Upvotes: 14
Views: 4649
Reputation: 99
Below is a solution for my use case with SwiftData. Initially, all the checks were done in one expression.
static func predicate(searchText: String, entryType: SafeBox.EntryTypeId = .all) -> Predicate<SafeBoxEntryModel> {
// Using "SafeBox.CategoryId.acctAndId.rawValue" in the expression causes this error,
// "Cannot infer key path type from context; consider explicitly specifying a root type".
let acctCat = SafeBox.CategoryId.acctAndId.rawValue
let pred1 = #Predicate<SafeBoxEntryModel> { sbEntry in
sbEntry.category == acctCat
}
let pred2 = #Predicate<SafeBoxEntryModel> { sbEntry in
sanitizedSearchText.isEmpty
}
// Look at actual attrs.
let pred3 = #Predicate<SafeBoxEntryModel> { sbEntry in
sbEntry.uniqueKey.localizedStandardContains(sanitizedSearchText)
|| sbEntry.desc.localizedStandardContains(sanitizedSearchText)
|| sbEntry.username.localizedStandardContains(sanitizedSearchText)
|| sbEntry.email.localizedStandardContains(sanitizedSearchText)
}
let pred4 = #Predicate<SafeBoxEntryModel> { sbEntry in
sbEntry.searchableBlob.localizedStandardContains(sanitizedSearchText)
|| sbEntry.note.localizedStandardContains(sanitizedSearchText)
}
return #Predicate<SafeBoxEntryModel> { sbEntry in
pred1.evaluate(sbEntry)
&& (pred2.evaluate(sbEntry) || pred3.evaluate(sbEntry) || pred4.evaluate(sbEntry))
}
}
Upvotes: 0
Reputation: 3439
I've build a library that implements this for predicate expression supported by SwiftData. (If you're targeting iOS 17.4 (or equivalent) there are also instructions on how to do it the new builtin way)
https://github.com/NoahKamara/CompoundPredicate/
Building on the answer by @orgtre
TLDR: This Gist implements two methods conjunction()
and disjunction()
on Array<Predicate<T>>
the reason for the error and subsequent crash is that the PredicateExpressions.Variable
is used to resolve the Predicate input.
This is how Predicate Variable reolving works internally:
The Predicate you create looks something like this (when expanded):
let predicate = Foundation.Predicate<Person>({
PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.name
),
PredicateExpressions.build_Arg("Luke")
)
})
The closure takes parameters of PredicateExpressions.Variable<Input>
which you need to pass as an argument to your expression $0
This variable will be unique for every predicate you created, meaning when you combine them using just the predicate.expression
property, each expression has a distinct Variable leading to a unresolved Variable
error.
I created a custom StandardPredicateExpression
that takes a predicate and a variable and will do the following in it's evaluate method:
struct VariableWrappingExpression<T>: StandardPredicateExpression {
let predicate: Predicate<T>
let variable: PredicateExpressions.Variable<T>
func evaluate(_ bindings: PredicateBindings) throws -> Bool {
// resolve the variable
let value: T = try variable.evaluate(bindings)
// bind the variable of the predicate to this value
let innerBindings = bindings.binding(predicate.variable, to: value)
// evaluate the expression with those bindings
return try predicate.expression.evaluate(innerBindings)
}
}
Extending the excellent work by @orgtre to create a solution that takes an array of predicates and a closure for combining them
extension Predicate {
typealias Expression = any StandardPredicateExpression<Bool>
static func combining<T>(
_ predicates: [Predicate<T>],
nextPartialResult: (Expression, Expression) -> Expression
) -> Predicate<T> {
return Predicate<T>({ variable in
let expressions = predicates.map({
VariableWrappingExpression<T>(predicate: $0, variable: variable)
})
guard let first = expressions.first else {
return PredicateExpressions.Value(true)
}
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
nextPartialResult($0,$1)
}
return expressions.dropFirst().reduce(first, closure)
})
}
}
let compound = Predicate<Person>.combine([predicateA, predicateB]) {
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
}
return Predicate<T>.combining(self, nextPartialResult: {
buildConjunction(lhs: $0, rhs: $1)
})
}
Check this Gist for an implementation
Upvotes: 3
Reputation: 235
Generalizing the answer by nOk a bit using a suggestion on the Swift Forums I've had success with this procedure for combining SwiftData predicates:
First write out each predicate using the #Predicate macro and make sure they work individually (failures at runtime happen frequently even without compilation errors). Here is an example of a Predicate
:
let predicate1 = #Predicate<Item> { item in
...
}
Next, right-click on each use of #Predicate
and select "Expand Macro". This might take quite some time and if the expansion doesn't appear (this happened often for me), I've found that it might help to simplify the predicate by splitting it up, removing a few "||" or "&&" conditions, rewriting it in other ways, or transferring it to an essentially equivalent simpler dummy project.
An expanded #Predicate
macro looks something like this:
let predicate1 = Foundation.Predicate<Item>({ item in
PredicateExpressions.xxx(
...
)
})
Copy everything in the expansion except the first and last line and assign it to a constant:
let expression1 = PredicateExpressions.xxx( ... )
Repeat for each predicate. After seeing a few of these expansions you might also try to directly modify or write the PredicateExpression manually (this is essential for more complex cases the #Predicate
macro can't handle).
Once you have a bunch of predicate expressions stored in constants, say named expression1 to expression3, you can conditionally combine them into one Predicate
, for example by forming their conjunction like this:
func makePredicate(useExpression1: Bool, useExpression2: Bool, useExpression3: Bool) -> Predicate<Item> {
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
}
return Predicate<Item>({ item in
var conditions: [any StandardPredicateExpression<Bool>] = []
let expression1 = PredicateExpressions.xxx( ... )
let expression2 = PredicateExpressions.xxx( ... )
let expression3 = PredicateExpressions.xxx( ... )
if useExpression1 {
conditions.append(expression1)
}
if useExpression2 {
conditions.append(expression2)
}
if useExpression3 {
conditions.append(expression3)
}
guard let first = conditions.first else {
return PredicateExpressions.Value(true)
}
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
buildConjunction(lhs: $0, rhs: $1)
}
return conditions.dropFirst().reduce(first, closure)
})
}
let combinedPredicate = makePredicate(useExpression1: true, useExpression2: true, useExpression3: true)
A few notes:
Item
named item
.PredicateExpressions.Conjunction
with PredicateExpressions.Disjunction
in the function buildConjunction
(and preferably rename it) to combine the predicates using logical OR instead.let expression1 = predicate1.expression
it just crashes at runtime.#Predicate
macro like this and directly using PredicateExpressions
one can construct much more advanced predicates than the #Predicate
macro can handle. If one still runs into a "unable to type-check this expression in reasonable time"-error one could just split out any PredicateExpression
into a further constant.Upvotes: 8
Reputation: 3439
Wow. This was hard, but i've found a solution to my problem:
If we use PredicateExpression
instead of Predicate we can later build a predicate like this:
let expression = PredicateExpressions.Value(true)
let predicate = Predicate<String>({ input in
expression
})
The next step was injecting the input. I chose to just create a closure that takes a variable and returns an expression (because the initialiser that takes expressions does not provide a value, but a variable)
let variableExp = { (variable: PredicateExpressions.Variable<String>) in
let value = PredicateExpressions.Value("Hello There")
return PredicateExpressions.Equal(
lhs: variable,
rhs: value
)
}
let variablePredicate = Predicate<String>({ input in
variableExp(input)
})
Swift Data Model:
@Model
class Book {
@Attribute
var title: String
@Attribute
var lastReadDate: Date
init(title: String, lastReadDate: Date) {
self.title = title
self.lastReadDate = lastReadDate
}
}
Create Closures for the expressions we want to use.
typealias BookVariable = PredicateExpressions.Variable<Book>
typealias BookKeyPath<T> = PredicateExpressions.KeyPath<BookVariable,T>
let dateEquals = { (input: BookKeyPath<Date>, _ value: Date) in
return PredicateExpressions.Equal(
lhs: input,
rhs: PredicateExpressions.Value(value)
)
}
The actual filtering:
// Do we want to filter by date at all?
let filterByDate = true
// The date we want to test against
let testDate = Date.now
let variablePredicate = Predicate<Book>({ input in
// Wrap values
let shouldFilterByDate = PredicateExpressions.Value(filterByDate)
let alwaysTrue = PredicateExpressions.Value(true)
// Create date Expression with testDate
let dateExp = dateEquals(BookKeyPath(root: input, keyPath: \Book.lastReadDate), testDate)
// Predicate that ,
// if shouldFilterByDate evaluates to true returns dateExp result
// otherwise returns expression that evaluates to true
return PredicateExpressions.build_Conditional(
shouldFilterByDate,
dateExp,
alwaysTrue
)
})
let descriptor = FetchDescriptor(predicate: variablePredicate)
modelContext.fetch(descriptor)
Upvotes: 16