Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining Predicate in SwiftData

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 }

Context:

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.


Tried and failed:

Closure with Bool return:

let isFavorite = { (item: Item) in item.isFavorite }
let predicate = #Predicate<Item> { isFavorite($0) }
  • won't work because Predicate does not allow global functions
  • i also tried creating an object IsFavoriteFilter with an evaluate(Item) -> Bool method but I also can't use that

I 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

like image 248
nOk Avatar asked Dec 07 '25 02:12

nOk


1 Answers

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)
})

Example with Model

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)
like image 136
nOk Avatar answered Dec 09 '25 18:12

nOk