Search code examples
swiftswiftuikeyswiftdataswift-keypath

Crash when sorting SwiftData Query with a SortDescriptor made from a KeyPath with an optional relationship in the chain


I have the following models:

@Model
class Game {
    var name: String
    var firstReleasedOn: Date?

    @Relationship(deleteRule: .cascade, inverse: \QueueEntry.game)
    var queueEntry: QueueEntry?

    init(
        name: String,
        firstReleasedOn: Date?
    ) {
        self.name = name
        self.firstReleasedOn = firstReleasedOn
    }
}
@Model
public class QueueEntry {
    var game: Game?
    var createdOn: Date
    var order: Int

    init(order: Int) {
        self.createdOn = .now
        self.order = order
    }
}

I want to populate a List with all entries sorted by their game's release date, so I have the following:

struct QueueView: View {
    @Query(sort: [SortDescriptor(\QueueEntry.game?.firstReleasedOn)])
    private var entries: [LocalData.QueueEntry]
    
    var body: some View {
        List(entries) { entry in
            Text(entry.game.name ?? "")
        }
    }
}

Running the above code in Debug is totally fine. Running in Release, however, throws the following error:

SwiftData/DataUtilities.swift:65: Fatal error: Couldn't find \QueueEntry.<computed 0x00000001049cd3e0 (Optional)>?.<computed 0x00000001049cd440 (Optional)> on QueueEntry with fields [SwiftData.Schema.PropertyMetadata(name: "game", keypath: \QueueEntry.<computed 0x00000001049e3814 (Optional)>, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "createdOn", keypath: \QueueEntry.<computed 0x00000001049e40e8 (Date)>, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "order", keypath: \QueueEntry.<computed 0x00000001049e49ac (Int)>, defaultValue: nil, metadata: nil)]

QueueEntry.game needs to be optional since making it non-optional and trying to create the relationship in the init results in an NSInvalidArgumentException, reasoning explained here by Joakim Danielson.

Is this just a bug/issue with SwiftData that needs to be addressed by Apple? Is there some other workaround I can implement that maintains the integrity of the relationship between Game and QueueEntry?


Solution

  • This answer doesn't solve the issue with sorting using an optional relationship in the keypath but gives an alternative solution for how to get the expected result.

    Since this is about a one-to-one relationship where as mentioned in the comments every QueueEntry must have a Game set even though the relationship is optional on both ends we can modify the view based on this information and work with Game instead.

    So using a Game query instead we can change the sort descriptor but we must also add a predicate to filter out and games without a QueueEntry

    @Query(filter: #Predicate<Game> { $0.queueEntry != nil }, 
           sort: [SortDescriptor(\Game.firstReleasedOn, order: .reverse)]) private var games: [Game]
    

    This will give us all QueueEntry objects in the database.

    Example list

    List(games) { game in
        VStack {
            HStack {
                Text(game.name)
                Text(game.queueEntry!.order.formatted())
            }
            Text(game.firstReleasedOn?.formatted() ?? "")
                .font(.caption)
        }
    }
    

    As an alternative if one still wants to use QueueEntry objects in the list is to add a computed property and use that for the List

    var entries: [QueueEntry] {
        games.compactMap(\.queueEntry)
    }
    

    To make it clearer that a QueueEntry always has a Game object we can change the init to take a non-optional Game

    init(order: Int, game: Game) {
        self.createdOn = .now
        self.order = order
        self.game = game
    }