Post not yet marked as solved
Post marked as unsolved with 2 replies, 207 views
I'm using SwiftData with SwiftUI. I want to disable a contextMenu button on a list item conditionally depending on the list item itself.
E.g.
I have a parent-child model as one-to-many relationship. On ParentListView, I need to disable the contextMenu button if parent.children is not empty.
Once user deletes the children in another ChildrenListView, switching back to the ParentView should now enable the delete button.
struct ParentListView: View {
@Environment(\.modelContext) private var modelContext
private var parents: [Parent]
// Intention: only allow delete if parent.children is empty
// And parent.children can be updated in another ChildrenView
@State private var disableDelete = true
var body: some View {
List(parents) { parent in
MyListItemView(parent)
.contextMenu {
Button {
if parent.children.isEmpty {
modelContext.delete(parent)
}
} label: {
Text("Delete")
}
.disabled(disableDelete)
// TODO: this doesn't work. Question: (a) why? (b) how to fix?
.onAppear {
if parent.children.isEmpty {
disableDelete = false
} else {
disableDelete = true
}
}
}
}
}
}
Question:
The onAppear seems to be triggered upon rendering the whole ParentListView, not the contextMenu button. Why? (and what's the general rule when this can happen?)
how to fix it? I think I'm flexible in terms of UI presentation. E.g. it doesn't have to be a contextMenu, it can be a button next to the list item, a command menu or something else suit for the job.
For context, I'm trying to find a workaround for SwiftData crash on deletion
with relation by manually implementing the deleteRule: .deny.
Appendix: MRE
ExampleModel.swift
import SwiftData
@Model
final class Parent {
@Attribute(.unique) var name: String
@Relationship(deleteRule: .deny, inverse: \Child.parent)
var children: [Child] = []
init(name: String) {
self.name = name
}
}
@Model
final class Child {
@Attribute(.unique) var name: String
var parent: Parent
init(name: String, parent: Parent) {
self.name = name
self.parent = parent
}
}
ExampleView.swift
import SwiftData
import SwiftUI
struct ExampleView: View {
@Environment(\.modelContext) private var modelContext
@Query private var parents: [Parent]
@Query private var children: [Child]
@State private var disableDelete = true
var body: some View {
VStack {
List(parents) { parent in
Text(parent.name)
.contextMenu {
Button {
modelContext.delete(parent)
} label: {
Text("Delete")
}
// TODO: this doesn't work. Question: (a) why? (b) how to fix?
.disabled(disableDelete)
.onAppear {
if !parent.children.isEmpty {
disableDelete = true
} else {
disableDelete = false
}
}
}
}
Spacer()
Button {
addNewParent()
} label: {
Text("Add parent")
.frame(maxWidth: .infinity)
.bold()
}
.background()
Divider()
List(children) { child in
Text("\(child.name) from: \(child.parent.name)")
.contextMenu {
Button {
modelContext.delete(child)
} label: {
Text("Delete")
}
}
}
Spacer()
Button {
addNewChild()
} label: {
Text("Add child")
.frame(maxWidth: .infinity)
.bold()
}
.background()
}
}
func addNewParent() {
let newParent = Parent(
name: "New Parent " + Int.random(in: 1...100).description
)
modelContext.insert(newParent)
}
func addNewChild() {
let newChild = Child(
name: "New Child " + Int.random(in: 1...100).description
,
parent: parents.randomElement()!
)
modelContext.insert(newChild)
}
}
#Preview {
ExampleView()
.modelContainer(
for: [
Parent.self,
Child.self
], inMemory: true
)
}
How to reproduce:
AddParent
Right click parent, expect delete button enabled
AddChild
Right click parent, expect delete button disabled
Screenshot: https://i.stack.imgur.com/AAvD6.png