Search code examples
iosswiftuiswiftdataipados

SwiftData deletions not triggering a View update in SplitNavigationView - iPad


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")
        }
    }
}

Solution

  • 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)