Search code examples
iosswiftswiftuiswiftdata

SwiftData - Creating a #Predicate with optional one to many relationship results in FatalError Thread 1: "to-many key not allowed here"


I haven't been able to find a solution to a predicate that filters on an optional one to many relationship. The relationship needs to be optional in order to work with CloudKit. I've recreated this with a very simple project. Let's assume I'm looking for a parents with a kid by the name of 'Abbijean'.

Model.swift

@Model
class Parent {
    let identifier: UUID
    let name: String
    @Relationship(deleteRule: .cascade, inverse: \Kid.parent) var children: [Kid]?
    init( name: String, children: [Kid]? = nil) {
        self.identifier = UUID()
        self.name = name
    }
}

@Model
class Kid {
    let identifier: UUID
    let name: String
    var parent: Parent?
    init(name: String, parent: Parent? = nil) {
        self.identifier = UUID()
        self.name = name
    }
}

ContentView.swift

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query(filter: #Predicate<Parent> { parent in
//        parent.children?.contains(where: {
//            $0.name == "Abbiejean"
//        }) != nil
        parent.children.flatMap { children in
            children.contains(where: { $0.name == "Abbijean" })
        } == true

    }) var parents: [Parent]
    var body: some View {
        Button(action: {
            var parent1 = Parent(name: "Sterling Archer")
            parent1.children?.append(Kid(name: "Abbijean"))
            modelContext.insert(parent1)
        }, label: {
            Text("Add Data")
        })
        List {
            ForEach(parents, id: \.id) { parent in
                Text("\(parent.name)")
            }
        }
    }
}

When using:

parent.children?.contains(where: {
   $0.name == "Abbijean"
}) != nil

The compiler tells me to use a flatMap when unwrapping optionals

Using a flat map:

parent.children.flatMap { children in
    children.contains(where: { $0.name == "Abbijean" })
} == true

The app crashes with: Thread 1: "to-many key not allowed here"

Tested in Xcode 15.0.1 (iOS 17.0.1) and 15 Beta 3 (iOS 17.2)


Solution

  • A predicate on a to-many relationship isn't possible it seems so you need to divide this into two steps,

    First a query on the to-one side of the relationship

    @Query(filter: #Predicate<Kid> { child in
        child.name == "Abbijean"
    }) var children: [Kid]
    

    And then a computed property to get the parents from the children returned by the query

    var parents: [Parent] {
        Set(children.compactMap(\.parent)).sorted(using: KeyPathComparator(\.name))
    }