Let's create a simple app using two SwiftData models.
@Model
final class Item {
@Attribute(.unique) var timestamp: Date
var tag: Tag?
init(timestamp: Date, tag: Tag? = nil) {
self.timestamp = timestamp
self.tag = tag
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var items: [Item]
init(name: String, items: [Item]) {
self.name = name
self.items = items
}
}
A ModelContainer
is injected into all windows.
@main
struct ModelActorDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self, Tag.self])
}
}
A ModelActor
is created to inject some sample data.
@ModelActor
final actor DataManager {
var idByTagName = [String : PersistentIdentifier]()
var idByItemDate = [Date : PersistentIdentifier]()
func run() async throws {
for name in ["foo", "bar", "baz"] {
let tag = Tag(name: name, items: [])
modelContext.insert(tag)
idByTagName.updateValue(tag.id, forKey: name)
}
try modelContext.save()
for date in [Date.now, Date.now, Date.now] {
let item = Item(timestamp: date, tag: nil)
modelContext.insert(item)
idByItemDate.updateValue(item.id, forKey: date)
}
try modelContext.save()
for (name, id) in idByTagName {
let tag = self[id, as: Tag.self]!
let item = self[idByItemDate.randomElement()!.value, as: Item.self]!
tag.items.append(item)
}
try modelContext.save()
}
}
So that DataManager
can run into a task
.
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
NavigationStack {
// …
}
.task {
let dataManager = DataManager(modelContainer: modelContext.container)
do {
try await dataManager.run()
} catch {
debugPrint(error)
fatalError(error.localizedDescription)
}
}
}
}
Running this causes a CoreData/SwiftData error. Why are tags and items empty?
CoreData: annotation: repairing validation failure Error Domain=NSCocoaErrorDomain Code=1560 "Multiple validation errors occurred." UserInfo={NSDetailedErrors=( "Error Domain=NSCocoaErrorDomain Code=1570 "%{PROPERTY}@ is a required value." UserInfo={NSValidationErrorObject=<NSManagedObject: 0x600000c3fc00> (entity: Item; id: 0x600002fe93e0 x-coredata:///Item/t07C086EC-9380-4BB6-97C5-9263B27DF24E7; data: {\n tag = "0x600002ffa060 x-coredata:///Tag/t07C086EC-9380-4BB6-97C5-9263B27DF24E2";\n timestamp = nil;\n}), NSLocalizedDescription=%{PROPERTY}@ is a required value., NSValidationErrorKey=timestamp, NSValidationErrorValue=null}", "Error Domain=NSCocoaErrorDomain Code=1550 "%{PROPERTY}@ is not valid." UserInfo={Dangling reference to an invalid object.=null, NSValidationErrorValue=<NSManagedObject: 0x600000c3f340> (entity: Tag; id: 0x600002ffa060 x-coredata:///Tag/t07C086EC-9380-4BB6-97C5-9263B27DF24E2; data: {\n items = (\n "0x600002fe93e0 x-coredata:///Item/t07C086EC-9380-4BB6-97C5-9263B27DF24E7"\n );\n name = nil;\n}), NSAffectedObjectsErrorKey=(\n "<NSManagedObject: 0x600000c3f340> (entity: Tag; id: 0x600002ffa060 x-coredata:///Tag/t07C086EC-9380-4BB6-97C5-9263B27DF24E2; data: {\n items = (\n \"0x600002fe93e0 x-coredata:///Item/t07C086EC-9380-4BB6-97C5-9263B27DF24E7\"\n );\n name = nil;\n})"\n), NSValidationErrorObject=<NSManagedObject: 0x600000c3fc00> (entity: Item; id: 0x600002fe93e0 x-coredata:///Item/t07C086EC-9380-4BB6-97C5-9263B27DF24E7; data: {\n tag = "0x600002ffa060 x-coredata:///Tag/t07C086EC-9380-4BB6-97C5-9263B27DF24E2";\n timestamp = nil;\n}), NSLocalizedDescription=%{PROPERTY}@ is not valid., NSValidationErrorKey=tag, NSValidationErrorShouldAttemptRecoveryKey=true}" )}
There are two things at play here, the first one is that your cache (dictionary) isn't valid and the other looks like a bug in ModelActor and/or ModelContext.
When creating a model instance and insert it into a model context the id
property is generated and set but this value is only temporary and will change to a permanent id once save()
is called on the ModelContext.
This is clearly seen if we print the property in the debugger, first for the unsaved object
p idByTagName["foo"]
(SwiftData.PersistentIdentifier?) some {
id = (url = "x-coredata:///Tag/t6A8A0E75-DE4B-45D5-83E1-4AEA970ED56C8")
implementation = 0x0000600002f65140 {
storeIdentifier = nil
isTemporary = true
URIRepresentation = "x-coredata:///Tag/t6A8A0E75-DE4B-45D5-83E1-4AEA970ED56C8"
primaryKey = "t6A8A0E75-DE4B-45D5-83E1-4AEA970ED56C8"
entityName = "Tag"
managedObjectID = 0x0000600000baef60 {
... (edited out)
}
}
}
and then the saved object
id = (url = "x-coredata://7EF034C1-B220-42A9-86ED-555E79BA6936/Tag/p18")
implementation = 0x0000600002f17d20 {
storeIdentifier = some {
_guts = {
_object = (_countAndFlagsBits = 13835058055282163748, _object = 0x4000600001e63940)
}
}
isTemporary = false
URIRepresentation = "x-coredata://7EF034C1-B220-42A9-86ED-555E79BA6936/Tag/p18"
primaryKey = "p18"
entityName = "Tag"
managedObjectID = 0x805629559f5bd665 {}
}
}
You can clearly see the difference (note that the id property has an id property!). So storing away the PersistentIdentifier
value for an unsaved model object is a bad idea.
Now the second part of this is that let tag = self[id, as: Tag.self]!
actually returns an object (that seems empty) for some reason instead of nil. Here you should have gotten nil back and a crash because of force unwrapping a nil value. So this must be a bug in SwiftData as I mentioned earlier.
A quick fix for this is to move try modelContext.save()
inside the loop before you add the id
to the dictionary so it is the final version you use.
A better solution is to not use any dictionaries at all and instead fetch all Item and Tag objects before the third loop where you connect them.