Search code examples
arraysswiftouter-joinswiftdata

Is it possible to join two SwiftData models into a single list?


I have three models SwiftData - a parent Event related to two others. I am trying to write a query that joins the two sub-models and filters to only show the results that match the selected event.

MODELS

@Model
class Event: Identifiable {
    var id = UUID()
    var name: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()
    
    @Relationship(deleteRule: .cascade)
    var flights: [Item1]?
    var events: [Item2]?
}

@Model
class Item1: Identifiable {
    var id = UUID()
    var name: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()
    
    @Relationship(inverse: \Trip.flights) var trip: Trip?
}

@Model
class Item2: Identifiable {
    var id = UUID()
    var name: String = ""
    var address: String = ""
    var notes: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()
    
    @Relationship(inverse: \Trip.events) var trip: Trip?
}

All of this is easy enough with a single model. I have a let variable defining the event and then focus on one of the items in the ForEach.

let event: Event

ForEach(event.item1!)

But I can't join the two models into a single variable without a host of errors.

I've tried a basic && in the ForEach but that gives an error that the array can't be converted to expected argument type Bool.

ForEach(event.item1! && event.item2!)

I've also tried a couple of queries but that has it's own errors.

@Query(filter: #Predicate<Item1>{ ($0.trip == trip) }) var item1: [Item1]
    @Query(filter: #Predicate<Item2>{ ($0.trip == trip) }) var item2: [Item2]
    let mergedEvents = item1 + item2
    

Each query line gets an error that "Instance member 'event' cannot be used on type 'EventDetails'; did you mean to use a value of this type instead?" And the final variable has an error that "Binary operator '+' cannot be applied to operands of type '[Item1]' and '[Item2]'"

The end result I'm going for is a single list with both Item1 and Item2 on it, sorted by a common field "startDate."

If I could write straight sql I'd write a straightforward JOIN with a WHERE clause to limit the results to the event but I have no idea how to handle it in Swift.

Any suggestions?


Solution

  • You can't join two @Query or join the results of @Query directly but what you can do is to create a protocol containing the properties that you want to show in your list and then let the model types conform to the protocol

    protocol EventItem {
        var id: UUID { get } 
        var name: String { get }
        var dateStart: Date { get }
        var dateEnd: Date { get }
    }
    
    extension Item1: EventItem {}
    extension Item2: EventItem {}
    

    Then you can combine the two properties flights and events into an array of type [any EventItem]

    Here I did it as a computed property in an extension to Event

    extension Event {
        var allItems: [any EventItem] {
            ((events ?? []) + (flights ?? [])).sorted { $0.dateStart < $1.dateStart }
        }
    }
    

    and then you can use this array in a view

    List(event.allItems, id: \.id) { item in
        HStack {
            Text(item.name)
            Spacer()
            Text(item.dateStart.formatted(date: .abbreviated, time: .omitted))
            Text(item.dateEnd.formatted(date: .abbreviated, time: .omitted))
        }
    }