swiftswiftuiswiftdata

Error saving SwiftData context after inserting items: Thread 1: EXC_BREAKPOINT


In my app I need to preload some data into the app when it first opens, as well as allow the user to restore that data within the app (through a button). For example, I have a ContentView with a list of MyModel items, and a toolbar button to restore the default items.

My model:

@Model
class MyModel {
    var title: String
    init(title: String) {
        self.title = title
    }
    
    static let defaults = [
        MyModel(title: "Pork"),
        MyModel(title: "Chicken"),
        MyModel(title: "Lamb"),
        MyModel(title: "Shortbread"),
    ]
}

Being as we need access to the model's container in a variety of different places, and call special methods on it, I created a special class to hold it called ContainerManager. It has a shared instance & stores the context & container when initialized internally, plus, populates the container with default MyModel items.

ContainerManager:

@MainActor
class ChipContainerManager {
    var container: ModelContainer
    
    private init() {
        container = try! ModelContainer(for: MyModel.self)
        
        if containerIsEmpty() {
            addDefaultChips()
        }
    }
    
    static let shared = ChipContainerManager()
    
    
    func containerIsEmpty() -> Bool {
        do {
            let chipFetch = FetchDescriptor<MyModel>()
            return try container.mainContext.fetch(chipFetch).isEmpty
        } catch {
            fatalError("Failed to check if container is empty: \(error)")
        }
    }
    
    
    func restContainerToDefaults() {
        try! container.erase()
        addDefaultChips()
    }
    
    
    func addDefaultChips() {
        MyModel.defaults.forEach { chip in
            container.mainContext.insert(chip)
        }
        
        do {
            try container.mainContext.save()
        } catch {
            print("Error saving context after adding default chips: \(error)")
        }
    }
}

In my view I access the list of items, allow the user to delete them or add a new one, and reset the container back to its default state by calling the ContainerManager method.

ContentView & Entry

@main
struct iOS_AppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView2()
                .modelContainer(ChipContainerManager.shared.container)
                .modelContext(ChipContainerManager.shared.container.mainContext)
        }
    }
}


struct ContentView: View {
    @Query var models: [MyModel]
    @Environment(\.modelContext) private var context
    
    var body: some View {
        NavigationStack {
            List(models) { m in
                HStack {
                    Text(m.title)
                    Button("Delete", systemImage: "trash") {
                        context.delete(m)
                    }
                }
            }
            .toolbar {
                ToolbarItem {
                    Button("Reset", action: ChipContainerManager.shared.restContainerToDefaults)
                }
                ToolbarItem {
                    Button("New item") {
                        context.insert(MyModel(title: "I added this one"))
                    }
                }
            }
        }
    }
}

The problem:

When running the app for the first time, SwiftData happily saves the context having added the default items. However when the user triggers the container reset from the button, it crashes giving the error above.

Recording:


Solution

  • You are using a completely new function on ModelContainer named erase() and looking at the documentation for it tells us nothing since that page is empty.

    When I run your code with debug logging enabled I see two messages printed to the console when erase() is called:

    CoreData: annotation: Disconnecting from sqlite database.

    error: Failed to delete support directory for store: /Users/.../Library/Developer/CoreSimulator/Devices/.../Library/Application Support/default.store

    So this implies to me like the erase() function does much more than deleting the persisted objects and also isn't completely successful in whatever it is trying to do.

    All in all this looks like a function not to use here and probably not at all before we have some documentation/explanation of what it is supposed to do.

    The solution you want here is to delete from the ModelContext

    The fastest way is to use delete(model:) but it will require that you update the UI manually

    func restContainerToDefaults() {
        try? container.mainContext.delete(model: MyModel.self)
        try? container.mainContext.save()
        addDefaultChips()
    }
    

    Another option is to fetch and delete all objects, which will behave better as far as updating the UI correctly

    func restContainerToDefaults() {
        for model in (try? container.mainContext.fetch(FetchDescriptor<MyModel>())) ?? [] {
            container.mainContext.delete(model)
        }
        try? container.mainContext.save()
        addDefaultChips()
    }