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?
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With