I had no trouble implementing a pop-up button that lets a user select from a mutually exclusive list of options. This is covered in the Pop-up buttons section of the HIG.
Now I want something similar but to allow the user to select any number of options from the list. The "Pop-up buttons" page in the HIG states:
Use a pull-down button instead if you need to: [...] Let people select multiple items
But the Pull-down buttons page of the HIG makes no mention of how to support multiple selection.
Here's what I tried so far. I start with the pop-up button code (copy and paste into an iOS Swift Playground to play along):
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()
Then update the code to make it a pull-down button. First, disable the changesSelectionAsPrimaryAction property of the button.
button.changesSelectionAsPrimaryAction = false
Then give the button a title so it appears as more than a tiny little square.
buttonConfig.title = "Select Items"
Now we have a button that shows a menu when it's tapped. But now there are no checkmarks and selecting a menu doesn't result in any checkmark. So here I thought I would update the handler block of the UIAction to toggle the action's state.
let action = UIAction(title: $0) { action in
print("Selected \(action.title)")
action.state = action.state == .on ? .off : .on
}
But now when you tap on a menu item the code crashes with an exception. When running in a real iOS app (not a Playground), the error is:
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'
Is it possible to implement a multiple select menu using UIButton and UIMenu?
If so, what piece am I missing?
If not, what component should be used for multiple selection? Ideally it would be great if the user could tap the button to bring up the menu, select multiple items in the menu, then tap the button again to dismiss the menu.
I found a work-around. Instead of trying to modify the
actionprovided in theUIActionhandler, you need to modify the original action in the button's menu.The problem is that you can't get the
UIMenufrom theUIAction. And you can't declare theUIMenubefore creating the array ofUIAction. So you need to create theUIButtonfirst, then you can access the button in the action handler which then lets you access the button's menu and update the matching action.Here's updated code that allows you to select multiple items in a pull-down menu:
Now that I finally figured out how to make this work, I realize it has two big user experience issues. 1) The button doesn't reflect the current selection state like the pop-up button does. 2) If the user wants to select more than one item, the user needs to tap the button to present the menu for each desired selection.
Issue 1 can be solved by adding the following line inside the action handler, after the code that updates the action's state:
Issue 2 can be solved, sort of, by adding the
.keepsMenuPresentedattribute to each of theUIActioninstances. While doing this allows the user to tap on multiple actions while the menu stays presented, the checkmark state of the actions does not change until the menu is dismissed and presented again.The original question and this answer don't address one other real-world part of this task - having some of the menu items selected initially. I leave that as an exercise for the reader.