SwiftUI ForEach列表切换按钮时自动关闭其他弹出菜单
实现方案
核心问题是每行独立维护弹出状态,互相之间没有状态同步,只要把弹出状态的控制权提升到父列表视图做统一管理,用单一数据源管控所有行的弹出状态,就能实现点击新行时自动关闭其他行已打开的菜单。
具体修改步骤
- 在父列表视图
RecipeFullListView中新增状态,存储当前处于弹出菜单激活状态的行唯一标识,用可选值实现,nil代表没有任何菜单打开 - 移除行视图
RecipeFullListRow内部独立维护的showRecipeOptions状态,改为接收父视图传递的激活状态绑定、当前行标识,点击按钮时同步更新父视图的全局激活状态 - 适配弹出菜单组件
ReditorPopUp的绑定逻辑,保证菜单内的关闭操作能同步更新全局状态
修改后代码
1. 父列表视图修改
struct RecipeFullListView: View { @Environment(\.dismiss) var dismiss @State var listofRecipes: [RecipeListModel] = RecipeList.recipes // 新增:当前激活弹出菜单的行索引,nil为无菜单打开 @State private var activePopUpRow: Int? = nil init(){ UITableView.appearance().backgroundColor = .clear } var body: some View { ZStack{ VStack{ Text("Recipes") .font(.title2) List{ ForEach(Array(listofRecipes.enumerated()), id: \.element.name){ index, recipe in RecipeFullListRow( recipe: recipe, recipeName: recipe.name, index: index, // 传递绑定给子行 activePopUpRow: $activePopUpRow ) } } // 可选优化:点击列表空白区域关闭所有弹出菜单 .onTapGesture { activePopUpRow = nil } } } } }
2. 列表行视图修改
struct RecipeFullListRow: View { let recipe: RecipeListModel let recipeName: String let index: Int // 接收父视图传递的全局激活状态绑定 @Binding var activePopUpRow: Int? // 计算属性:判断当前行是否要显示弹出菜单 private var showRecipeOptions: Bool { activePopUpRow == index } var body: some View { ZStack{ Text(recipeName) .font(.body) Text("10g 25g 88g") .foregroundColor(.gray) .padding(.top, 80) .padding(.bottom, 10) .padding(.trailing, 10) .frame(height:90) if showRecipeOptions{ ReditorPopUp(shown: Binding( get: { activePopUpRow == index }, set: { newValue in activePopUpRow = newValue ? index : nil } )) .padding(.top, 20) .padding(.leading, 15) } } .padding(.top, -10) .padding(5) Button(action: { // 点击按钮时切换当前行状态:如果当前行已激活就关闭,否则设为激活,自动覆盖其他行的激活状态 if activePopUpRow == index { activePopUpRow = nil } else { activePopUpRow = index } }){ Image(systemName: "slider.horizontal.3") .padding(.top, 10) .foregroundColor(.black) } .buttonStyle(BorderlessButtonStyle()) } }
3. 弹出菜单组件适配
原有逻辑基本无需改动,只需要保证操作后能同步更新绑定状态即可:
struct ReditorPopUp: View { @Binding var shown: Bool @State var showEditRecipe = false var body: some View { if shown{ ZStack{ VStack (alignment: .leading, spacing: 20){ HStack(spacing:12){ Image(systemName: "pencil") .font(.title2) Button(action:{ showEditRecipe.toggle() }){ Text("Edit") .foregroundColor(.black) } .buttonStyle(BorderlessButtonStyle()) .fullScreenCover(isPresented: $showEditRecipe){ RecipeEditor() } } HStack(spacing: 12){ Image(systemName: "trash") .font(.title2) .foregroundColor(.red) Button(action: { // 点击删除后直接关闭菜单 shown = false }){ Text("Delete") .foregroundColor(.black) } .buttonStyle(BorderlessButtonStyle()) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) .transition(.backslide) .animation(.easeInOut(duration: 0.25)) } } }
效果说明
现在所有行的弹出状态由父视图统一管控:
- 点击任意行按钮时,会更新全局的激活行值,之前激活的行因为判断不匹配会自动隐藏弹出菜单
- 再次点击同一行的按钮,会把激活值设为
nil,关闭当前行菜单 - 可选添加的空白点击手势,可以实现点击菜单外区域自动关闭菜单的交互体验
- 如果你的数据模型有唯一id字段,也可以把
activePopUpRow的类型从Int?改成对应id类型,避免列表重排序时索引错位的问题

内容的提问来源于stack exchange,提问作者Swink
相关产品推荐
相关产品推荐

