You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI+SwiftData:如何根据列表项状态条件禁用contextMenu按钮?

问题:SwiftData+SwiftUI中根据列表项状态条件禁用ContextMenu删除按钮

我用SwiftData搭配SwiftUI开发,需要根据列表项自身状态条件禁用ContextMenu里的删除按钮。场景是一对多的父子模型:在ParentListView中,当parent.children不为空时禁用删除按钮;用户在ChildrenListView删除子项后返回ParentView时,要重新启用该按钮。

我的初始代码如下:

struct ParentListView: View {
  @Environment(\.modelContext) private var modelContext
  private var parents: [Parent]

  // 意图:仅当parent.children为空时允许删除
  // parent.children可能在另一个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: 这里不生效,问题:(a) 为什么?(b) 怎么修复?
          .onAppear {
            if parent.children.isEmpty {
              disableDelete = false
            } else {
              disableDelete = true
            }
          }
        }
    }
  }
}

现有问题

  1. onAppear似乎在整个ParentListView渲染时触发,而非ContextMenu按钮显示时,原因是什么?此类情况的通用规则是什么?
  2. 如何修复该问题?UI形式可灵活调整(比如改成列表项旁按钮、命令菜单等)。

背景说明

尝试手动实现deleteRule: .deny,解决SwiftData关联数据删除时的崩溃问题。截至2024年4月9日,临时方案是把子模型的var parent: Parent改成可选类型var parent: Parent?,并使用deleteRule: .cascade或nullify,同时在代码中手动限制删除规则。


最小可复现示例(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: 这里不生效,问题:(a) 为什么?(b) 怎么修复?
            .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
    )
}

复现步骤

  • 添加Parent
  • 右键点击Parent,期望删除按钮可用
  • 添加Child
  • 右键点击Parent,期望删除按钮不可用

问题解答

1. 为什么onAppear在列表渲染时触发而非ContextMenu显示时?

onAppear是SwiftUI视图生命周期的回调,它会在视图首次渲染到屏幕上时触发,而非ContextMenu弹出时。ContextMenu的内容属于宿主视图的一部分,会在宿主视图初始化时就完成渲染(即使未显示),所以当列表项加载时,ContextMenu里的按钮已经完成初始化,onAppear就执行了。

通用规则

  • onAppear/onDisappear针对的是视图本身的显示/隐藏状态,不管它是普通视图还是弹窗、菜单的内容。
  • 菜单、弹窗这类"惰性显示"的组件,其内容视图会在宿主视图渲染时完成初始化,而非触发显示时。

2. 修复方案

核心思路是直接绑定到parent.children.isEmpty状态,不需要额外的@State变量——因为SwiftData的@Model对象是可观察的,当子项变化时会自动通知视图更新。

方案1:直接在disabled中判断状态

修改ContextMenu中的按钮部分,去掉@State var disableDelete,直接用parent.children.isEmpty的取反作为禁用条件:

List(parents) { parent in
  Text(parent.name)
    .contextMenu {
      Button {
        modelContext.delete(parent)
      } label: {
        Text("Delete")
      }
      // 直接根据子项是否为空判断禁用状态
      .disabled(!parent.children.isEmpty)
    }
}

当parent.children变化时(比如删除子项),SwiftData会自动更新视图,按钮的禁用状态也会实时刷新。

方案2:复杂逻辑下使用@ObservedObject(可选)

如果后续有更复杂的禁用条件,可以显式将Parent对象标记为观察对象(SwiftData的@Model默认已可观察,通常无需额外处理):

List(parents) { parent in
  @ObservedObject var obsParent = parent
  Text(obsParent.name)
    .contextMenu {
      Button {
        modelContext.delete(obsParent)
      } label: {
        Text("Delete")
      }
      .disabled(!obsParent.children.isEmpty)
    }
}

额外优化:删除操作的安全校验

即使按钮被禁用,建议在删除逻辑中再做一次校验,防止意外情况:

Button {
  guard parent.children.isEmpty else { return }
  modelContext.delete(parent)
} label: {
  Text("Delete")
}
.disabled(!parent.children.isEmpty)

SwiftData删除规则补充

如果使用deleteRule: .deny时崩溃,除了前端校验,也可以调整模型的关联规则:将Child的parent设为可选类型,配合deleteRule: .nullify:

@Model
final class Child {
  @Attribute(.unique) var name: String
  // 改为可选类型
  var parent: Parent?

  init(name: String, parent: Parent) {
    self.name = name
    self.parent = parent
  }
}

同时Parent的Relationship调整为:

@Relationship(deleteRule: .nullify, inverse: \Child.parent)
var children: [Child] = []

这样删除Parent时,子项的parent会被设为nil,不会触发崩溃,配合前端的禁用逻辑,就能实现类似.deny的效果。

内容的提问来源于stack exchange,提问作者Weishi Z

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 04:20:53