How to handle predicate with optionals in SwiftData

Xcode: 15.1

I've got (simplified) model with relationship many-to-many defined as follows

@Model
final class Item {
    var title: String
    @Relationship(inverse: \Tag.items) var tags: [Tag]?

    init(_ title: String) {
        self.title = title
    }
}

@Model
final class Tag {
    var name: String
    var items: [Item]?

    init(_ name: String) {
        self.name = name
    }
}

and a view with a query

struct ItemsView: View {
    @Query var items: [Item]

    var body: some View {
        List {...}
    }

    init(searchText: String) {
        _items = Query(filter: #Predicate<Item> { item in
            if (searchText.isEmpty) {
                return true
            } else {
                return item.tags!.contains{$0.name.localizedStandardContains(searchText)}
            }
        })
    }
}

This code compiles but fails at runtime with an error: Query encountered an error: SwiftData.SwiftDataError(_error: SwiftData.SwiftDataError._Error.unsupportedPredicate)

It looks like Predicate does not like optionals cause after changing tags and items to non optionals and the predicate line to

item.tags.contains{$0.name.localizedStandardContains(searchText)}

everything works perfectly fine.

So, my question is, does anybody know how to make it work with optionals?

Full code: https://github.com/m4rqs/PredicateWithOptionals.git

Answered by thibautrichez in 774913022

Sorry, I wasn't able to test my answer earlier and was confident that this kind of query worked but I end up with the same error: to-many key not allowed here. I tried other alternatives but couldn't find one that worked. Doesn't seem like it's possible to use optional to-many relationships inside a Predicate (even checking for $0.tags != nil or similar will crash)

A workaround would be to query on the to-one side of the relationship, i.e Tag and compute the array of Item from the results. It's not ideal but I couldn't find another way. Since it's working for non-optional relationships we can only hope that this will possible in future releases.

struct ItemsView: View {
    @Query private var tags: [Tag]

    var items: [Item] {
        self.tags.lazy.compactMap(\.items).flatMap { $0 }
    }

    init(searchText: String) {
        self._tags = Query(filter: #Predicate<Tag> {
            $0.name.localizedStandardContains(search)
        })
    }
}

As stated in the thread 742807, you should not force unwrap your properties inside the predicate. You could use flatMap to access the unwrapped array of tags

    init(searchText: String) {
        _items = Query(filter: #Predicate<Item> {
            if searchText.isEmpty {
                true
            } else {
                $0.tags.flatMap {
                    $0.contains { $0.name.localizedStandardContains(searchText) }
                } == true
            }
        })
    }

Thanks for the answer. Unfortunately this doesn't work. It falls over with "to-many key not allowed here" error.

Accepted Answer

Sorry, I wasn't able to test my answer earlier and was confident that this kind of query worked but I end up with the same error: to-many key not allowed here. I tried other alternatives but couldn't find one that worked. Doesn't seem like it's possible to use optional to-many relationships inside a Predicate (even checking for $0.tags != nil or similar will crash)

A workaround would be to query on the to-one side of the relationship, i.e Tag and compute the array of Item from the results. It's not ideal but I couldn't find another way. Since it's working for non-optional relationships we can only hope that this will possible in future releases.

struct ItemsView: View {
    @Query private var tags: [Tag]

    var items: [Item] {
        self.tags.lazy.compactMap(\.items).flatMap { $0 }
    }

    init(searchText: String) {
        self._tags = Query(filter: #Predicate<Tag> {
            $0.name.localizedStandardContains(search)
        })
    }
}

Thank you so much. It worked. This really puzzles me as the documentation https://developer.apple.com/documentation/foundation/predicate says that swift optionals can be freely used with predicates.

How to handle predicate with optionals in SwiftData
 
 
Q