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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 01:27:17