Hot to merge Predicate in SwiftData

In Core Data, I can merge two predicates (perform an OR operation) using the following code:

let compound3 = NSCompoundPredicate(type: .or, subpredicates: [compound1, compound2])

How can I achieve a similar operation in SwiftData with new Predicate?

Post not yet marked as solved Up vote post of Fat Xu Down vote post of Fat Xu
1.2k views

Replies

You can just combine these two predicates in the predicate builder. Assuming your compound1 looks like myEntity.field1 == true and compound2 looks like myEntity.field2 == false you can write

#Predicate { myEntity in
    myEntity.field1 == true || myEntity.field2 == false
}

for an AND variant you can do

#Predicate { myEntity in
    myEntity.field1 == true && myEntity.field2 == false
}

Thank you for your reply. However, in some cases, I need to prepare some predicates in advance and combine them according to some conditions during the running of the app. This is also the main use of NSCompoundPredicate in this situation. However, in the new Predicate, no similar mechanism is provided. I feel that PredicateExpression may provide a corresponding method, but I haven't found it yet.

Add a Comment

I got Orgtre's answer to the Stackoverflow: Combining Predicate in SwiftData to work (eventually).

You break your desired compound predicate into its simplest logic components. Then write a simple query like you normally do for SwiftData for each of the single predicates you want to compound, eg.

_books = Query(filter: #Predicate<Book> { $0.title.contains("the")}).
_books = Query(filter: #Predicate<Book> { $0.title.contains("tom")}). //etc

Then expand each one of these predicate's macros by right clicking the word "Predicate", e.g.

Foundation.Predicate<Book>({
    PredicateExpressions.build_contains(
        PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_Arg($0),
            keyPath: \.title
        ),
        PredicateExpressions.build_Arg("the")
    )
    })

Copy just the Predicate Expressions code the macro generates (not the Foundation.Predicate<Book> bit) and throw away the Query you used to generate the macro. Use the copied code to create a Predicate Expression ready for use inside Orgtre's makePredicate() function - you will need to swap the $0 for book or whatever variable you are using in your closure.

            let expression2 = PredicateExpressions.build_contains(
                    PredicateExpressions.build_KeyPath(
                        root: PredicateExpressions.build_Arg(book),
                        keyPath: \.title
                    ),
                    PredicateExpressions.build_Arg("the")
                )

Rinse and repeat for all the predicates you may need.

For a particular filtering task, append all the predicate expressions you need into an array. Orgtre's function then uses that array (called conditions in his example)to return a predicate (that is really a compound predicate) you can then use for the filter for your query as per normal SwiftData.

Orgtre's booleans (useExpression1) just toggle whether or not to append a particular predicate into the compound predicate at any given time that could be controlled by toggles in your UI.

My brain hurts after that paragraph so here is some code to sketch out the above paragraph that hopefully will make things less muddy.


import SwiftUI
import SwiftData

@Model
class Book {
    var title: String
    var lastReadDate: Date
    
    init(title: String = "", lastReadDate: Date = Date()) {
        self.title = title
        self.lastReadDate = lastReadDate
    }
}

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query var books: [Book]
    
    init(){
        let combinedPredicate = makePredicate(useExpression1: true, useExpression2: true)
        _books = Query (filter:  combinedPredicate)
    }
 
    var body: some View {
        VStack {
            Button("Add samples"){
                addSamples()
            }
            Text("Hi \(books.count)")
            List{
                ForEach(books){book in
                    HStack{
                        Text("\(book.title)")
                        Text("\(book.lastReadDate.ISO8601Format())")
                    }
                }
            }
        }
        .padding()
        .toolbar {
            Button("Add Samples", action: addSamples)
        }
    }
    
    func addSamples() {
        let book1 = Book(title: "the importance of being earnest tom", lastReadDate: Date())
        let book2 = Book(title: "the tom swift", lastReadDate: Date())
        let book3 = Book(title: "around the world in 80 days", lastReadDate: Date())
        modelContext.insert(book1)
        modelContext.insert(book2)
        modelContext.insert(book3)
    }
    func makePredicate(useExpression1: Bool, useExpression2: Bool) -> Predicate<Book> {

        func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
            PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
        }

        return Predicate<Book>({ book in
            var conditions: [any StandardPredicateExpression<Bool>] = []

            let expression1 =
            PredicateExpressions.build_contains(
                    PredicateExpressions.build_KeyPath(
                        root: PredicateExpressions.build_Arg(book),
                        keyPath: \.title
                    ),
                    PredicateExpressions.build_Arg("tom")
                )

            let expression2 = PredicateExpressions.build_contains(
                    PredicateExpressions.build_KeyPath(
                        root: PredicateExpressions.build_Arg(book),
                        keyPath: \.title
                    ),
                    PredicateExpressions.build_Arg("the")
                )

            if useExpression1 {
                conditions.append(expression1)
            }

            if useExpression2 {
                conditions.append(expression2)
            }
   
            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)
        })
    }
}

#Preview {
    ContentView()
}

My understanding of the intricacies of this code are limited, so please don't expect clever answers if you want to know more. I look forward to some future iteration of SwiftData when this will become a lot easier.

@Indubitably Thank you for your answer. I followed your instructions and it's working great - however - mine is acting like an AND predicate and not an OR.

If anyone is looking for OR:

Replace PredicateExpressions.Conjunction with PredicateExpressions.Disjunction in the function buildConjunction (and preferably rename it) to combine the predicates using logical OR instead.