如何实现支持多选功能的iOS下拉按钮(Pull-down Button)?
我已经成功实现支持互斥选项选择的弹出按钮(Pop-up Button),这在HIG的《Pop-up buttons》章节中有相关说明。现在我需要实现类似但支持多选的功能,HIG的弹出按钮页面指出:「若需支持多选等场景,请使用下拉按钮(Pull-down Button)」,但HIG的下拉按钮页面未提及如何实现多选。
我尝试的步骤如下:
先基于弹出按钮代码(可复制到iOS Swift Playground运行):
import UIKit import PlaygroundSupport class MyVC: UIViewController { override func viewDidLoad() { super.viewDidLoad() let items = [ "Option 1", "Option 2", "Option 3", "Option 4" ] let actions: [UIAction] = items.map { let action = UIAction(title: $0) { action in print("Selected \(action.title)") } return action } let menu = UIMenu(children: actions) var buttonConfig = UIButton.Configuration.gray() let button = UIButton(configuration: buttonConfig) button.menu = menu button.showsMenuAsPrimaryAction = true button.changesSelectionAsPrimaryAction = true button.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button) NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), ]) } } PlaygroundPage.current.liveView = MyVC()
随后修改代码转为下拉按钮:
- 禁用按钮的
changesSelectionAsPrimaryAction属性:
button.changesSelectionAsPrimaryAction = false
- 为按钮设置标题:
buttonConfig.title = "Select Items"
此时按钮点击可弹出菜单,但无选中标记,我尝试在UIAction的处理块中切换action的state:
let action = UIAction(title: $0) { action in print("Selected \(action.title)") action.state = action.state == .on ? .off : .on }
但点击菜单项时程序崩溃,真实iOS应用中的错误信息为:
2023-05-21 10:40:56.038217-0600 ButtonMenu[63482:10716279] *** Assertion failure in -[_UIImmutableAction setState:], UIAction.m:387
2023-05-21 10:40:56.063676-0600 ButtonMenu[63482:10716279] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Action is immutable because it is a child of a menu'
请问是否可以通过UIButton和UIMenu实现多选菜单?若可以,我遗漏了哪些步骤?若不行,应使用什么组件实现多选功能?理想效果为点击按钮弹出菜单,多选后再次点击按钮关闭菜单。
可以通过UIButton + UIMenu实现多选菜单,你之前的崩溃是因为UIAction加入菜单后会变为不可变对象,无法直接修改其state属性,且遗漏了开启菜单多选支持的关键配置。以下是正确的实现步骤:
关键修改点
- 开启菜单的多选支持:创建
UIMenu时,设置options为[.allowsMultipleSelection],系统会自动处理菜单项的选中状态切换,无需手动修改UIAction的state。 - 为UIAction设置唯一Identifier:方便后续追踪选中的项(可选,但推荐)。
- 维护选中状态(可选):如果需要在外部获取或展示选中的项,可以维护一个集合来记录选中的标识符或标题。
完整可运行代码
import UIKit import PlaygroundSupport class MyVC: UIViewController { // 维护选中项的标识符集合(可选,用于追踪选中状态) private var selectedIdentifiers = Set<UUID>() override func viewDidLoad() { super.viewDidLoad() let items = [ "Option 1", "Option 2", "Option 3", "Option 4" ] let actions: [UIAction] = items.map { item in let actionID = UUID() let action = UIAction(title: item, identifier: actionID) { [weak self] action in guard let self = self else { return } // 更新本地选中集合 if self.selectedIdentifiers.contains(actionID) { self.selectedIdentifiers.remove(actionID) } else { self.selectedIdentifiers.insert(actionID) } print("当前选中项:\(self.selectedIdentifiers.compactMap { id in items.first { item in action.id == id } })") } // 初始状态设置(可选,比如默认选中某些项) action.state = self.selectedIdentifiers.contains(actionID) ? .on : .off return action } // 创建支持多选的菜单 let menu = UIMenu( title: "", options: [.allowsMultipleSelection], // 关键:开启多选 children: actions ) var buttonConfig = UIButton.Configuration.gray() buttonConfig.title = "Select Items" let button = UIButton(configuration: buttonConfig) button.menu = menu button.showsMenuAsPrimaryAction = true button.changesSelectionAsPrimaryAction = false // 下拉按钮需禁用此属性 button.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button) NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), ]) } } PlaygroundPage.current.liveView = MyVC()
额外说明
- 系统会自动处理菜单项的选中/取消选中视觉反馈,无需手动修改
UIAction的state。 - 如果需要动态更新菜单内容(比如根据选中状态修改按钮标题),可以在选中集合变化后重新生成
UIMenu并赋值给button.menu。 - 若你的iOS版本低于14(不推荐),则无法使用
UIMenu的多选特性,此时可以考虑使用UITableView或UICollectionView自定义弹出菜单,或者使用第三方组件。
内容的提问来源于stack exchange,提问作者HangarRash

