15

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?

8
  • 1
    I'm having the same issue right now.. Did you find a solution, by any chance? Commented Jan 3, 2024 at 1:34
  • 1
    @SplittyDev Unfortunately not. I currently have a terrible ‘workaround’ in place where I’ve created a new gameFirstReleasedOn property on QueueEntry, and I assign the Game.firstReleasedOn date to that when I create new entry. Awful, and totally redundant data duplication, but I haven’t found anything better yet. Commented Jan 3, 2024 at 14:51
  • Thanks for the quick reply! Just saw it now.. I haven't found a fix either, I just went with querying everything and manually filtering and sorting it after.. Terrible for performance, but not much else I can do. I hope they fix it. Commented Jan 10, 2024 at 13:59
  • Exact same issue, also only on release. I’m also using a dedicated property combined with a didSet Commented Feb 1, 2024 at 22:05
  • 1
    Same for more. It's unfortunate as I'm having to manually perform the sorts now. Like Ben said, terrible for performance and very redundant... Weird it works fine on development builds. Boo! Commented Feb 10, 2024 at 7:22

1 Answer 1

1
+50

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
}
Sign up to request clarification or add additional context in comments.

2 Comments

A novel workaround! Even though it doesn't directly address the issue (as you admitted yourself) I'm going to mark it as the accepted solution since it seems like the best we've got for now given the limitations of SwiftData's queries and predicates. Thanks!
I didn’t mention it in my answer but I tried a lot of different solutions to make the keypath with optionals work but never found a solution. It’s an issue with SwiftData but also I think one-to-one relationships are a bit tricky.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.