SwiftData: This model instance was invalidated because its backing data could no longer be found the store

Hello 👋,

I encounter the "This model instance was invalidated because its backing data could no longer be found the store" crash with SwiftData. Which from what I understood means I try to access a model after it has been removed from the store (makes sense).

I made a quick sample to reproduce/better understand because there some case(s) I can't figure it out.

Let's take a concrete example, we have Home model and a Home can have many Room(s).

// Sample code
@MainActor
let foo = Foo() // A single reference
let database = Database(modelContainer: sharedModelContainer) // A single reference

@MainActor
class Foo {
    // Properties to explicilty keep reference of model(s) for the purpose of the POC
    var _homes = [Home]()
    var _rooms = [Room]()

    func fetch() async {
        let homes = await database.fetch().map {
            sharedModelContainer.mainContext.model(for: $0) as! Home
        }
				print(ObjectIdentifier(homes[0]), homes[0].rooms?.map(\.id)) // This will crash here or not.
    }
    
    // Same version of a delete function with subtle changes.
    // Depending on the one you use calling delete then fetch will result in a crash or not.

    // Keep a reference to only homes == NO CRASH
    func deleteV1() async {
        self._homes = await database.fetch().map {
            sharedModelContainer.mainContext.model(for: $0) as! Home
        }
        await database.delete()
    }
    
    // Keep a reference to only rooms == NO CRASH
    func deleteV2() async {
        self._rooms = await database.fetch().map {
            sharedModelContainer.mainContext.model(for: $0) as! Home
        }[0].rooms ?? []
        await database.delete()
    }
    
    // Keep a reference to homes & rooms == CRASH 💥
    func deleteV3() async {
        self._homes = await database.fetch().map {
            sharedModelContainer.mainContext.model(for: $0) as! Home
        }
				self._rooms = _homes[0].rooms ?? []
        // or even only retain reference to rooms that have NOT been deleted 🤔 like here "id: 2" make it crash
				// self._rooms = _homes[0].rooms?.filter { r in r.id == "2" } ?? []
        await database.delete()
    }
}

Calling deleteV<X>() then fetch() will result in a crash or not depending on the scenario.

I guess I understand deleteV1, deleteV2. In those case an unsaved model is served by the model(for:) API and accessing properties later on will resolve correctly. The doc says: "The identified persistent model, if known to the context; otherwise, an unsaved model with its persistentModelID property set to persistentModelID."

But I'm not sure about deleteV3. It seems the ModelContext is kind of "aware" there is still cyclic reference between my models that are retained in my code so it will serve these instances instead when calling model(for:) API ? I see my home still have 4 rooms (instead of 2). So I then try to access rooms that are deleted and it crash. Why of that ? I mean why not returning home with two room like in deleteV1 ?

Because SwiftData heavily rely on CoreData may be I miss a very simple thing here. If someone read this and have a clue for me I would be extremely graceful.


PS: If someone wants to run it on his machine here's some helpful code:

// Database
let sharedModelContainer: ModelContainer = {
    let schema = Schema([
        Home.self,
        Room.self,
    ])
    let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
    debugPrint(modelConfiguration.url.absoluteString.replacing("%20", with: "\\ "))
    return try! ModelContainer(for: schema, configurations: [modelConfiguration])
}()

extension Database {
    static let shared = Database(modelContainer: sharedModelContainer)
}

@ModelActor
actor Database {
    func insert() async {
        let r1 = Room(id: "1", name: "R1")
        let r2 = Room(id: "2", name: "R2")
        let r3 = Room(id: "3", name: "R3")
        let r4 = Room(id: "4", name: "R4")
        let home = Home(id: "1", name: "My Home")
        home.rooms = [r1, r2, r3, r4]

        modelContext.insert(home)
        try! modelContext.save()
    }

    func fetch() async -> [PersistentIdentifier] {
        try! modelContext.fetchIdentifiers(FetchDescriptor<Home>())
    }

    @MainActor
    func delete() async {
        let mainContext = sharedModelContainer.mainContext
        try! mainContext.delete(
            model: Room.self,
            where: #Predicate { r in
                r.id == "1" || r.id == "4"
            }
        )
        try! mainContext.save()

        // 🤔 Calling fetch here seems to solve crash too, force home relationship to be rebuild correctly ?
        //  let _ = try! sharedModelContainer.mainContext.fetch(FetchDescriptor<Home>())
    }
}

// Models
@Model
class Home: Identifiable {
    @Attribute(.unique) public var id: String
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Room.home)
    var rooms: [Room]?

    init(id: String, name: String, rooms: [Room]? = nil) {
        self.id = id
        self.name = name
        self.rooms = rooms
    }
}

@Model
class Room: Identifiable {
    @Attribute(.unique) public var id: String
    var name: String
    var home: Home?

    init(id: String, name: String, home: Home? = nil) {
        self.id = id
        self.name = name
        self.home = home
    }
}

When you are using @ModelActor you have access to a specific model context for that actor via the property modelContext so you should have

try modelContext.delete(…

and you should definitely not use the context from the main actor in any other actor.

This alone will probably not fix the crash since you have a reference to the deleted objects in self._rooms that you need to clear before doing the delete.

Hello @joadan, I made the sample as is to illustrate that may be the user will touch the mainContext to delete objects (by touching a user interface) even if a ModelActor exists. But may be that’s another topic. I update my sample and just tried using modelContext in delete and removing @MainActor but as you guess, still crashing. However manually clearing the self._rooms property (~= deleteV1) or even setting explicitly nil the home property of my rooms before deleting, avoid the crash.

When you say « you have a reference to the deleted objects in self._rooms that you need to clear before doing the delete. » This seems to be obvious to you can you detail a bit ? I guess this is pretty common scenario where an object like a view retain some models. Why does keeping a reference to homes is ok, rooms ok but both leads to a crash?

SwiftData: This model instance was invalidated because its backing data could no longer be found the store
 
 
Q