On an iPad, the leftmost pane (Sidebar) of a SplitNavigationView has a toolbar button that displays a sheet. This sheet can load or delete all SwiftData objects. These objects are listed in the second pane (Content). Hitting the load button creates some new sample data and displays it no problem. However hitting the delete button appears to have no effect and doesn't trigger a view update.
The model context is in the environment and the list is fed with an @Query. I've tried injecting the ModelContext into the SettingsSheet rather than accessing it through the environment but the result is the same. In fact on a relaunch the data is sometimes not even deleted.
I've cut the code down to the bare minimum to show the issue and it should just be a copy, paste and run. Am I misunderstanding how the SwiftData operations percolate through the environment? Does the presence of the sheet need handling differently? Any help gratefully received.
import SwiftData
import SwiftUI
@main
struct ProblemTestApp: App {
let container: ModelContainer
var body: some Scene {
WindowGroup {
NavigationSplitView {
SidebarView()
} content: {
ContentView()
} detail: {
DetailTabbedView()
}
.modelContainer(container)
}
}
init() {
let schema = Schema( [ Monkey.self ] )
let configuration = ModelConfiguration("ProblemTestApp", schema: schema)
do {
container = try ModelContainer(for: schema, configurations: configuration)
} catch {
fatalError("Could not configure the SwiftData container.")
}
}
}
// SidebarView
struct SidebarView: View {
@State private var showingSettingsSheet = false
var body: some View {
Text("Sidebar Monkey")
.toolbar {
Button {
showingSettingsSheet.toggle()
} label: {
Label("Show settings", systemImage: "gearshape")
}
}
.sheet(isPresented: $showingSettingsSheet) { /* On dismiss. */ } content: {
SettingsSheet()
}
}
}
// ContentView
struct ContentView: View {
@Query var allMonkeys: [Monkey]
var body: some View {
List {
Text("Monkey count = \(allMonkeys.count)")
ForEach(allMonkeys) { monkey in
Text(monkey.name)
}
}
}
}
// DetailTabbedView
struct DetailTabbedView: View {
var body: some View {
Text("Detail Tabbed View (tabs to come)")
}
}
// Monkey model
@Model
final class Monkey: Identifiable {
var id: UUID = UUID()
var name: String = ""
init(name: String) {
self.name = name
}
}
// SettingsSheet
struct SettingsSheet: View {
@Environment(\.modelContext) var context
var body: some View {
NavigationStack {
HStack {
Button("Load") {
for _ in 0...9 {
let monkey = Monkey(name: String(Int.random(in: 0...999)))
context.insert(monkey)
}
}
Button("Delete") {
do {
try context.delete(model: Monkey.self)
print("Deleted all the Monkeys")
} catch {
print("Failed to delete all Monkeys.")
}
}
}
.navigationTitle("Monkey Settings")
}
}
}
The problem is with the function you are using to delete.
When I run your code in the debugger I notice a few things of interest:
Directly after the call to context.delete(model: Monkey.self)
I can see in the debugger that the model context holds no deleted objects since the array property deletedModelsArray
is empty.
Furthermore if I add a fetch before the delete and then after the delete call examine the isDeleted
flag of an object then it is false
.
So clearly this method is some kind of batch method that does not update the state of the ModelContext
or any in-memory model objects and I assume this is for performance reasons which makes sense. This could have been better documented though.
For UI near operations I recommend you resort to individual deletes instead because then the ModelContext
object and your view will be properly updated.
let monkeys = try context.fetch(FetchDescriptor<Monkey>())
for monkey in monkeys {
context.delete(monkey)
}
or more compact if you prefer
try context.fetch(FetchDescriptor<Monkey>()).forEach(context.delete)