GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SOLVED: SwiftData - Trouble with Predicates.

Forums > Books

I'm updating my current apps to SwiftData and have run into a problem with adding filtering criteria to the predicate in my browse views. Basically my issue is I hava a relationship where entity A can have multiple of entity B so I want the B browse to filter by A BUT the value passed in is optional and I want no data showing if it's nil.

As I'm following Hacking with Swift I was able to create the issue with the iTour code included with the SwiftData book so you can better see what I mean. Here Destination is the entity A mentioned before and each one has multiple Sights (B).

The error (after a compile wait): Cannot convert value of type 'PredicateExpressions.Conditional<PredicateExpressions.Equal<PredicateExpressions.Value<Optional<Destination>>, PredicateExpressions.NilLiteral<Destination>>, PredicateExpressions.StringLocalizedStandardContains<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Array<Sight>.Element>, String>, PredicateExpressions.Value<String>>, PredicateExpressions.Conditional<PredicateExpressions.KeyPath<PredicateExpressions.Value<String>, Bool>, PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Array<Sight>.Element>, Destination?>, PredicateExpressions.Value<Destination?>>, PredicateExpressions.Conjunction<PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Array<Sight>.Element>, Destination?>, PredicateExpressions.Value<Destination?>>, PredicateExpressions.StringLocalizedStandardContains<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Array<Sight>.Element>, String>, PredicateExpressions.Value<String>>>>>' (aka 'PredicateExpressions.Conditional<PredicateExpressions.Equal<PredicateExpressions.Value<Optional<Destination>>, PredicateExpressions.NilLiteral<Destination>>, PredicateExpressions.StringLocalizedStandardContains<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Sight>, String>, PredicateExpressions.Value<String>>, PredicateExpressions.Conditional<PredicateExpressions.KeyPath<PredicateExpressions.Value<String>, Bool>, PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Sight>, Optional<Destination>>, PredicateExpressions.Value<Optional<Destination>>>, PredicateExpressions.Conjunction<PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Sight>, Optional<Destination>>, PredicateExpressions.Value<Optional<Destination>>>, PredicateExpressions.StringLocalizedStandardContains<PredicateExpressions.KeyPath<PredicateExpressions.Variable<Sight>, String>, PredicateExpressions.Value<String>>>>>') to closure result type 'any StandardPredicateExpression<Bool>'

To demonstate I changed the code in SightsView to the following and, for demonstation, only pass in nil:


import SwiftUI
import SwiftData

struct SightsView: View {

    @State private var path = [Sight]()
    @State private var sortOrder = [SortDescriptor(\Sight.name)]
    @State private var searchText = ""

    var body: some View {
        NavigationStack(path: $path) {
            SightsListingView(sort: sortOrder, searchString: searchText, destination: nil)
                .navigationTitle("Sights")
                .searchable(text: $searchText)
        }
    }
}

struct SightsListingView: View {
    @Environment(\.modelContext) var modelContext

    @Query var sights: [Sight]

    init(sort: [SortDescriptor<Sight>], searchString: String, destination: Destination?) {
        _sights = Query(filter: #Predicate {
            //            if searchString.isEmpty{
            //                return true
            //            } else {
            //                return  $0.name.localizedStandardContains(searchString)
            //            }

            if destination == nil {
                return $0.name.localizedStandardContains(searchString)
                //                            below assuming it's equal now si
            } else if searchString.isEmpty {
                return $0.destination == destination
            } else {
                return $0.destination == destination && $0.name.localizedStandardContains(searchString)
            }

            //            if destination == nil && searchString.isEmpty {
            //                return true
            //            } else if destination == nil {
            //                return $0.name.localizedStandardContains(searchString)
            //            } else if searchString.isEmpty {
            //                return $0.destination == destination
            //            } else {
            //                return $0.destination == destination && $0.name.localizedStandardContains(searchString)
            //            }
        }, sort: sort)
    }

    var body: some View {
        List {
            ForEach(sights) { sight in
//                NavigationLink(value: sight) {
                    VStack(alignment: .leading) {
                        Text(sight.name)
                            .font(.headline)
                    }
//                }
            }
            .onDelete(perform: deleteSights)
        }
    }

    func deleteSights(_ indexSet: IndexSet) {
        for index in indexSet {
            let sight = sights[index]
            modelContext.delete(sight)
        }
    }
}

Thanks for any help.

3      

Update:

The code isn't pretty but I made some changes to the predicate and got it to compile and run. That said, when I made the same change to my main code with the main entity passed in (A or in this example Destination) it still ran but received this error: Query encountered an error: SwiftData.SwiftDataError(_error: SwiftData.SwiftDataError._Error.unsupportedPredicate) and didn't get any entries in the list althougth the app didn't crash and add still worked though didn't show up.

Current code is:

init(sort: [SortDescriptor<Sight>], searchString: String, destination: Destination?) {
        if let thisID = destination?.id {
            if searchString.isEmpty {
                _sights = Query(filter: #Predicate {
                    return $0.destination!.id == thisID
                }, sort: sort)
            } else {
                _sights = Query(filter: #Predicate {
                    return $0.destination!.id == thisID && $0.name.localizedStandardContains(searchString)
                }, sort: sort)
            }
        } else {
            _sights = Query(filter: #Predicate {
                if searchString.isEmpty{
                    return true
                } else {
                    return  $0.name.localizedStandardContains(searchString)
                }
            }, sort: sort)
        }
    }

4      

init(sort: [SortDescriptor<Sight>], searchString: String, destination: Destination?) { if let thisID = destination?.id { if searchString.isEmpty { _sights = Query(filter: #Predicate { return $0.destination!.id == thisID }, sort: sort) } else { _sights = Query(filter: #Predicate { return $0.destination!.id == thisID && $0.name.localizedStandardContains(searchString) }, sort: sort) } } else { _sights = Query(filter: #Predicate { if searchString.isEmpty fontsbunch.com { return true } else { return $0.name.localizedStandardContains(searchString) } }, sort: sort) } }

3      

Crazy Idea that I'm assuming isn't right as I'm getting these errors at runtime:

Error: this application, or a library it uses, has passed an invalid numeric value (NaN, or not-a-number) to CoreGraphics API and this value is being ignored. Please fix this problem.
If you want to see the backtrace, please set CG_NUMERICS_SHOW_BACKTRACE environmental variable.

But if I switch the query to binding (state didn't refresh) I can make use of the relationship.


    @Binding var sights: [Sight]

    init(sort: [SortDescriptor<Sight>], searchString: String, destination: Destination?) {
        if destination == nil {
            _sights = .constant([Sight]())
        } else if searchString.isEmpty {
            _sights = .constant(destination?.sights ?? [Sight]())
        } else {
            _sights = .constant((destination?.sights ?? [Sight]()).filter( { $0.name.localizedStandardContains(searchString) }))
        }
    }

4      

Small update. Going to use the hacky way but it updates on edits but not adds.

4      

Got it working in case anyone has this problem. Apparently you can't use another entity in the predicate.

Got it working with the answer to this StackOverflow question: https://stackoverflow.com/questions/77039981/swiftdata-query-with-predicate-on-relationship-model

And posted a longer how to on my personal blog at SimplyKyra.com (can't include the link but it's titled "SwiftData: Solving Filtering by an Entity in the Predicate"). That said, included the fixed code here first with the...

Working init code:

  init(sort: [SortDescriptor<Sight>], searchString: String, destination: Destination?) {
        if let thisID = destination?.persistentModelID {
            _sights = Query(filter: #Predicate<Sight> { currSight in
                if searchString.isEmpty {
                    return currSight.destination?.persistentModelID == thisID
                } else {
                    return currSight.destination?.persistentModelID == thisID && currSight.name.localizedStandardContains(searchString)
                }
            }, sort: sort)
        } else {
            _sights = Query(filter: #Predicate<Sight> { currSight in
                // if id is nil than I don't want ANYTHING
                return false
//                if searchString.isEmpty {
//                    return true
//                } else {
//                    return currSight.name.localizedStandardContains(searchString)
//                }
            }, sort: sort)
        }
    }

StackOverflow question with https://stackoverflow.com/users/3377161/joel answer:

You gave me an idea and it worked !!!

Considering that you can't use another model in the predicate, then first set a variable with the persistentModelID and use that variable in the predicate.

I was having the same problem and this worked for me. Of course you need to set the query in your init()

EDIT: I added part of the code that could be helpful.

@Query private var homes: [Home]

init(session: Session) {

    let id = session.persistentModelID
    let predicate = #Predicate<Home> { home in
        home.session?.persistentModelID == id
    }

    _homes = Query(filter: predicate, sort: [SortDescriptor(\.timestamp)] )
}

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.