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.
@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.
@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.
@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"))
}
}
}
}
}
}
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.
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()
}