Reputation: 593
This is an additional scenario for the question and answer covered here.
Sample code is available here.
Original problem was related to this predicate:
let aPred = #Predicate<Event> {
$0.dateStart != nil &&
($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) ||
($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) ||
($0.dateStart < startOfDay && $0.dateEnd >= startOfDay)
}
This was giving compiling error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.
This can be addressed with the help of this extension: (credit Mojtaba Hosseini)
extension Optional: Comparable where Wrapped: Comparable {
public static func < (lhs: Wrapped?, rhs: Wrapped?) -> Bool {
guard let lhs, let rhs else { return false }
return lhs < rhs
}
}
Now the problem is that if I add to my @Model final class TestEvent
one more optional bool var isActive: Bool?
I get the same error again.
let aPred = #Predicate<TestEvent> {
$0.isActive == true && //comment this out and it will compile
$0.dateStart != nil &&
($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) ||
($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) ||
($0.dateStart < startOfDay && $0.dateEnd >= startOfDay)
}
Conditions:
Ideal solution will be a possibility to break the predicate down into smaller predicates and combine all them in a similar way as it is possible in CoreData using NSCompoundPredicate
After updating XCode to 15.4 CompoundPredicate.swift
compiles and I am able to combine predicates using .conjunction()
however the fetch return empty results, specifically:
func mainPred(_ aDate: Date) -> Predicate<Event> {
let startOfDay = aDate.startOfDay
let endOfDay = aDate.endOfDay
let datesPred = #Predicate<Event> {
$0.dateStart != nil &&
($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) ||
($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) ||
($0.dateStart < startOfDay && $0.dateEnd >= startOfDay)
}
let activePred = #Predicate<Event> {
$0.isActive == true
}
return [activePred, datesPred].conjunction()
}
func fetchData() {
var events = [Events]()
let descriptor = FetchDescriptor<Event>(predicate: mainPred(aDate))
do {
events = try context.fetch(descriptor)
} catch {
print(error)
}
//events is empty
}
Now if I use just single dates predicate and then filter fetched array using the same condition, it will fetch matching objects as expected.
func mainPred(_ aDate: Date) -> Predicate<Event> {
let startOfDay = aDate.startOfDay
let endOfDay = aDate.endOfDay
let datesPred = #Predicate<Event> {
$0.dateStart != nil &&
($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) ||
($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) ||
($0.dateStart < startOfDay && $0.dateEnd >= startOfDay)
}
return datesPred
}
func fetchData() {
var events = [Events]()
let descriptor = FetchDescriptor<Event>(predicate: mainPred(aDate))
do {
events = try context.fetch(descriptor)
} catch {
print(error)
}
events = events.filter {
$0.isActive == true
}
//events has fetched objects
}
Am I misunderstanding something obvious?
Upvotes: 1
Views: 135
Reputation: 119312
If you are able to expand the macro, you can see why the compiler can not type check on time! This is what the macro implemented for that 4 line:
Foundation.Predicate<TodoModel>({
PredicateExpressions.build_Disjunction(
lhs: PredicateExpressions.build_Disjunction(
lhs: PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_NotEqual(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateStart
),
rhs: PredicateExpressions.build_NilLiteral()
),
rhs: PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateStart
),
rhs: PredicateExpressions.build_Arg(startOfDay),
op: .greaterThanOrEqual
),
rhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateStart
),
rhs: PredicateExpressions.build_Arg(endOfDay),
op: .lessThanOrEqual
)
)
),
rhs: PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateEnd
),
rhs: PredicateExpressions.build_Arg(startOfDay),
op: .greaterThanOrEqual
),
rhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateEnd
),
rhs: PredicateExpressions.build_Arg(endOfDay),
op: .lessThanOrEqual
)
)
),
rhs: PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateStart
),
rhs: PredicateExpressions.build_Arg(startOfDay),
op: .lessThan
),
rhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.dateEnd
),
rhs: PredicateExpressions.build_Arg(startOfDay),
op: .greaterThanOrEqual
)
)
)
})
Although it is so amazing and powerful, Unfortunately this power let the Apple engineers bring a total mess like this into the production :)
So based on these extreme expanded implementation, you can implement functions similar to build_Conjunction
and build_Disjunction
to merge SwiftData predicates:
(👇 You just need this file to make it enable 👇)
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
/// Allows you to use an existing Predicate as a ``StandardPredicateExpression``
struct VariableWrappingExpression<T>: StandardPredicateExpression {
let predicate: Predicate<T>
let variable: PredicateExpressions.Variable<T>
func evaluate(_ bindings: PredicateBindings) throws -> Bool {
// resolve the variable
let value = try variable.evaluate(bindings)
// create bindings for the expression of the predicate
let innerBindings = bindings.binding(predicate.variable, to: value)
return try predicate.expression.evaluate(innerBindings)
}
}
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
extension Predicate {
typealias Expression = any StandardPredicateExpression<Bool>
/// Returns the result of combining the predicates using the given closure.
///
/// - Parameters:
/// - predicates: an array of predicates to combine
/// - nextPartialResult: A closure that combines an accumulating expression and
/// an expression of the sequence into a new accumulating value, to be used
/// in the next call of the `nextPartialResult` closure or returned to
/// the caller.
/// - Returns: The final accumulated expression. If the sequence has no elements,
/// the result is `initialResult`.
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)
})
}
}
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
public extension Array {
/// Joins multiple predicates with an ``PredicateExpressions.Conjunction``
/// - Returns: A predicate evaluating to true if **all** sub-predicates evaluate to true
func conjunction<T>() -> Predicate<T> where Element == Predicate<T> {
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)
})
}
/// Joins multiple predicates with an ``PredicateExpressions.Disjunction``
/// - Returns: A predicate evaluating to true if **any** sub-predicate evaluates to true
func disjunction<T>() -> Predicate<T> where Element == Predicate<T> {
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
PredicateExpressions.Disjunction(lhs: lhs, rhs: rhs)
}
return Predicate<T>.combining(self, nextPartialResult: {
buildConjunction(lhs: $0, rhs: $1)
})
}
}
Than you can break down the predicate into smaller chunks like:
let activePredicate = #Predicate<TodoModel> {
$0.isActive == true
}
let nullCheckPredicate = #Predicate<TodoModel> {
$0.dateStart != nil && $0.dateEnd != nil
}
let rangesPredicates = #Predicate<TodoModel> {
($0.dateStart >= startOfDay && $0.dateStart <= endOfDay) ||
($0.dateEnd >= startOfDay && $0.dateEnd <= endOfDay) ||
($0.dateStart < startOfDay && $0.dateEnd >= startOfDay)
}
and conjunct
and disjunct
them like:
[activePredicate, nullCheckPredicate, rangesPredicates].conjunction() // 👈 Use this for AND `&&` predicates
[activePredicate, nullCheckPredicate, rangesPredicates].disjunction() // 👈 Use this for OR `||` predicates
🎮 Here is a working playground containing all you need to test
As mentioned above, the gist file can be founded here and credits goes to NoahKamara, nOk and other contributors who developed these powerful extensions originally.
Also, this article may help you more with understanding of this!
Upvotes: 4