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