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.
2
Answers
I found a work-around. Instead of trying to modify the
action
provided in theUIAction
handler, you need to modify the original action in the button's menu.The problem is that you can't get the
UIMenu
from theUIAction
. And you can't declare theUIMenu
before creating the array ofUIAction
. So you need to create theUIButton
first, 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:
I don't know a good way to solve issue 2. I'll leave that for another question.
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.
It does sound like the stuff in the pop-up menu section of the HIG about using the pull-down menu for multiple selection is just blowing chunks. Not the first time I’ve seen that happen in Apple’s docs, WWDC videos, etc.
I would suggest just using a different interface, i.e. don’t try to do this with the built-in button -> UIMenu interface.
Instead, when the user does whatever the user should do (tap the button), you present a presented view controller; now you’re in charge of the interface, and you can use a multiple-selection table view, a table view with switches, whatever. (You can see some rather old discussion about some of the options here.) The presented view can be at any size and location; I like to use a popover presentation for this sort of thing.
I think that’s better than "fighting the framework".