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 wholeParentListView
, 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