You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SwiftUI iOS中是否支持ContextMenu的多选配置实现?

SwiftUI iOS中是否支持ContextMenu的多选配置实现?

嗨,我之前在做类似原生照片App的功能时也卡过这个问题!确实,截至iOS 17,SwiftUI原生还没有直接对应UIKit里contextMenuConfigurationForItemsAt的多选上下文菜单API,但咱们可以通过组合现有API来实现一模一样的效果,我给你捋捋具体怎么做~

核心思路

先手动管理元素的选中状态,再通过长按/右键手势触发对应批量操作的菜单:

  • 无选中元素时,长按进入多选模式并弹出批量菜单
  • 已有选中元素时,长按直接弹出批量菜单
  • 支持点击切换单个元素的选中状态
  • 顶部工具栏同步显示选中状态和操作入口

完整代码示例

先定义一个简单的照片数据模型:

struct Photo: Identifiable {
    let id = UUID()
    let name: String
}

然后是实现多选上下文菜单的网格视图:

struct PhotoGridView: View {
    // 模拟照片数据
    @State private var photos: [Photo] = (1...20).map { Photo(name: "照片\($0)") }
    // 跟踪选中的照片ID集合
    @State private var selectedPhotos: Set<Photo.ID> = []
    // 控制批量菜单的显示/隐藏
    @State private var showMultiMenu = false
    // 菜单的弹出位置
    @State private var menuPosition: CGPoint = .zero

    var body: some View {
        NavigationStack {
            LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 10) {
                ForEach(photos) { photo in
                    GeometryReader { geoProxy in
                        Image(systemName: "photo")
                            .resizable()
                            .scaledToFit()
                            .padding()
                            // 选中状态的背景高亮
                            .background(selectedPhotos.contains(photo.id) ? Color.blue.opacity(0.3) : Color.gray.opacity(0.1))
                            .cornerRadius(8)
                            // 点击逻辑:无选中时触发单元素操作,有选中时切换选中状态
                            .onTapGesture {
                                if selectedPhotos.isEmpty {
                                    print("查看「\(photo.name)」详情")
                                } else {
                                    selectedPhotos.toggle(photo.id)
                                }
                            }
                            // 长按触发多选菜单
                            .onLongPressGesture(minimumDuration: 0.2, perform: {
                                // 无选中时先把当前元素加入选中集合
                                if selectedPhotos.isEmpty {
                                    selectedPhotos.insert(photo.id)
                                }
                                // 获取长按位置,让菜单弹在元素附近
                                let globalFrame = geoProxy.frame(in: .global)
                                menuPosition = CGPoint(
                                    x: globalFrame.midX,
                                    y: globalFrame.midY
                                )
                                showMultiMenu = true
                            })
                    }
                    .frame(height: 120)
                }
            }
            .padding()
            .navigationTitle("我的相册")
            // 选中状态下的工具栏
            .toolbar {
                if !selectedPhotos.isEmpty {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("取消") {
                            selectedPhotos.removeAll()
                        }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Text("\(selectedPhotos.count) 项选中")
                            .font(.subheadline)
                    }
                }
            }
            // 批量操作菜单
            .sheet(isPresented: $showMultiMenu, onDismiss: {
                // 菜单消失后保留多选状态,方便继续操作
            }) {
                Menu {
                    Button(role: .destructive) {
                        // 删除选中的照片
                        photos.removeAll { selectedPhotos.contains($0.id) }
                        selectedPhotos.removeAll()
                    } label: {
                        Label("删除选中项", systemImage: "trash")
                    }

                    Button {
                        print("分享 \(selectedPhotos.count) 张照片")
                        selectedPhotos.removeAll()
                    } label: {
                        Label("分享选中项", systemImage: "square.and.arrow.up")
                    }

                    Button {
                        print("标记选中项为收藏")
                    } label: {
                        Label("添加到收藏", systemImage: "star")
                    }
                } label: {
                    EmptyView()
                }
                .menuStyle(.borderlessButton)
                .position(menuPosition)
            }
        }
    }
}

优化细节

  • 菜单位置精准化:通过GeometryReader获取元素的全局坐标,让菜单弹在长按位置附近,完全贴合原生体验
  • 多设备适配:如果是iPadOS支持鼠标/触控板,还可以添加onContextMenuGesture,逻辑和长按手势完全一致,实现右键触发批量菜单
  • 状态联动:工具栏的选中计数和取消按钮会自动根据选中状态显示/隐藏,和原生App逻辑对齐

替代方案:UIKit桥接

如果你更倾向于用UIKit的原生UIContextMenuConfiguration来实现,也可以通过UIViewRepresentable把UIKit的集合视图包装到SwiftUI中,直接复用contextMenuConfigurationForItemsAt的逻辑,但这种方式需要处理SwiftUI和UIKit的数据绑定,相对繁琐一些,上面的纯SwiftUI方案已经能满足大部分场景啦~

火山引擎 最新活动