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
}