iosswiftuicloudkitswiftdata

List won't update when item is deleted


I have two selectable lists, the first is populated with Category model objects and the second with Subcategory model objects (which are taken from selected Category value of the first list).

When I delete an item from the first list, the second list does not update. Here is a video that shows what I mean: https://youtube.com/shorts/VJCe_UK0TNA

I've coded the selectable lists so that the selected object is retrieved from a computed property binding that returns the object with the most recent selected date:

private var selectedCategoryBinding: Binding<Category1?>
{
    return Binding<Category1?>(get: { self.categories.max(by: { ($0.lastSelectedDate) < ($1.lastSelectedDate) })}, set:{_ in})
}

private var selectedSubcategoriesBinding: Binding<[Subcategory1]?>
{
    Binding<[Subcategory1]?>(get: { self.selectedCategoryBinding.wrappedValue?.subcategories }, set: { self.selectedCategoryBinding.wrappedValue?.subcategories = $0 })
} 

I fear that this may be where the problem lies, but I'm unsure how to fix it. Below is the complete code. Any help would be appreciated.

import SwiftUI
import SwiftData

protocol SelectableListItem: Equatable, Identifiable, PersistentModel {
    var name: String { get set }
    var lastSelectedDate: Date { get set }
}

@Model
final class Project1 {
    var name: String = ""
    @Relationship(deleteRule: .cascade, inverse: \Category1.project) var categories: [Category1]?
    
    init(name: String) {
        self.name = name
    }
}

@Model
final class Category1: SelectableListItem {
    var name: String = ""
    @Relationship(deleteRule: .cascade, inverse: \Subcategory1.category) var subcategories: [Subcategory1]?
    var project: Project1?
    var lastSelectedDate: Date = Date.now
    
    init(name: String) {
        self.name = name
    }
}

@Model
final class Subcategory1: SelectableListItem {
    var name: String = ""
    var category: Category1?
    var lastSelectedDate: Date = Date.now
    
    init(name: String) {
        self.name = name
    }
}

struct HomeView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var queriedProjects: [Project1]

    var body: some View {
        NavigationStack {
            VStack {
                List {
                    ForEach(queriedProjects) { project in
                        if let categoriesBinding = Binding(
                            Binding<[Category1]?>(
                                get: { project.categories },
                                set: { project.categories = $0 }
                            )
                        ) {
                            NavigationLink(
                                destination: ProjectView1(project: project, categories: categoriesBinding),
                                label: { Text(project.name) }
                            )
                        }
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            let project = Project1(name: "Project 1")
                            
                            let subcategory1 = Subcategory1(name: "Subcategory 1")
                            let subcategory2 = Subcategory1(name: "Subcategory 2")
                            let subcategory3 = Subcategory1(name: "Subcategory 3")
                            let subcategory4 = Subcategory1(name: "Subcategory 4")
                            
                            let category1 = Category1(name: "Category 1")
                            category1.subcategories = [subcategory1, subcategory2]
                            
                            let category2 = Category1(name: "Category 2")
                            category2.subcategories = [subcategory3, subcategory4]
                            
                            project.categories = [category1, category2]
                            modelContext.insert(project)
                        } label: {
                            Label("Plus", systemImage: "plus")
                        }
                    }
                }
            }
            .overlay {
                if queriedProjects.isEmpty {
                    Text("Tap the + button to create a new project")
                }
            }
            .task {
                do {
                    try modelContext.delete(model: Project1.self)
                } catch {
                    fatalError(error.localizedDescription)
                }
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

struct ProjectView1: View {
    @Bindable var project: Project1
    @Binding var categories: [Category1]
    
    @Environment(\.editMode) private var editMode
    @Environment(\.modelContext) private var modelContext
    
    @State private var columnVisibility: NavigationSplitViewVisibility = .all

    private var selectedCategoryBinding: Binding<Category1?> {
        Binding<Category1?>(
            get: { categories.max(by: { $0.lastSelectedDate < $1.lastSelectedDate }) },
            set: { _ in }
        )
    }
    
    private var selectedSubcategoriesBinding: Binding<[Subcategory1]?> {
        Binding<[Subcategory1]?>(
            get: { selectedCategoryBinding.wrappedValue?.subcategories },
            set: { selectedCategoryBinding.wrappedValue?.subcategories = $0 }
        )
    }
    
    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            SelectableList(items: $categories) { category in
                category.project = nil
            }
            
            if !categories.isEmpty {
                if let subcategoriesBinding = Binding(selectedSubcategoriesBinding) {
                    SelectableList(items: subcategoriesBinding) { subcategory in
                        subcategory.category = nil
                    }
                }
            }
        } detail: {
            EmptyView()
        }
    }
}

struct SelectableList<T: SelectableListItem>: View {
    @Binding var items: [T]
    private(set) var deleteItem: (T) -> Void
    
    @Environment(\.modelContext) private var modelContext
    @Query private var queriedItems: [T]
    
    private var filteredAndSortedItems: [T] {
        queriedItems
            .filter { items.contains($0) }
            .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
    }
    
    private var selectedItem: T? {
        filteredAndSortedItems.max(by: { $0.lastSelectedDate < $1.lastSelectedDate })
    }
    
    var body: some View {
        List {
            ForEach(filteredAndSortedItems) { item in
                HStack {
                    Button {
                        if selectedItem != item {
                            item.lastSelectedDate = .now
                        }
                    } label: {
                        Text(item.name)
                    }
                    .buttonStyle(PlainButtonStyle())
                                                                                                
                    if selectedItem == item {
                        Image(systemName: "checkmark")
                    }
                    
                    Spacer()
                    
                    Button {
                        deleteItem(item)
                        modelContext.delete(item)
                    } label: {
                        Image(systemName: "trash")
                    }
                }
            }
        }
    }
}

Solution

  • The problem was in the way I was deleting the item in the SelectableList closure private(set) var deleteItem: (T) -> Void:

    SelectableList(items: $categories)
    {
        category in category.project = nil
    }
    

    and:

    SelectableList(items: subcategoriesBinding)
    {
        subcategory in subcategory.category = nil
    }
    

    Kudos to Andrei G. for helping me realize that deleting in this manner isn't going to provide an immediate update, and that in fact it has be done directly from the @Binding var items: [T] in SelectableList:

    Button
    {
        //deleteItem(item)
        if let index = self.items.firstIndex(where: { $0 == item })
        {
            self.items.remove(at: index)
        }
              
        self.modelContext.delete(item)
    }