Reputation: 27427
How to change the state for an UIAction? The goal is to toggle a state checkmark next to an UIAction inside UIMenu.
Changing a UIAction's state
via a reference stored in the view controller does not seem to change the state at all. Am I missing anything?
// View Controller
internal var menuAction: UIAction!
private func generatePullDownMenu() -> UIMenu {
menuAction = UIAction(
title: "Foo",
image: UIImage(systemName: "chevron.down"),
identifier: UIAction.Identifier("come.sample.action"),
state: .on
) { _ in self.menuAction.state = .off } // <--- THIS LINE
let menu = UIMenu(
title: "Sample Menu",
image: nil,
identifier: UIMenu.Identifier("com.sample.menu"),
options: [],
children: [menuAction]
)
return menu
}
// Inside UI setup code block
let buttonItem = UIBarButtonItem(
title: "",
image: UIImage(systemName: "chevron.down"),
primaryAction: nil,
menu: generatePullDownMenu()
)
Tried to change the action
state from the closure directly and got the "Action is immutable because it is a child of a menu" error. Now I suspect an action object is always an immutable object.
menuAction = UIAction(
title: "Foo",
image: UIImage(systemName: "chevron.down"),
identifier: UIAction.Identifier("come.sample.action"),
state: .on
) { action in action.state = .off } // <--- THIS LINE
Upvotes: 22
Views: 9004
Reputation: 19
The UIMenu supports a check mark specifying an item on/off. As the OP specified, they cannot figure out how a user can select an menu item and the check mark toggles.
How to change the state for an UIAction? The goal is to toggle a state checkmark next to an UIAction inside UIMenu.
This is possible today by closing the menu, then re-evaluating the UIMenu (as the answers above state). The check mark will then correctly reflect the on/off state.
But as of iOS 16.0, the menu itself can stay visible while items are selected, and their UIAction is invoked.
if (@available(iOS 16.0, *)) {
[menu setAttributes:UIMenuElementAttributesKeepsMenuPresented];
}
What I assumed the OP wanted was for the menu item's check box to change as the user selected an item, and while the menu was still displayed.
I cannot seem to get this to work, confirming as the OP mentions the UIAction is a mutable object. So the checkmark cannot be changed in the UIAction.
So what's missing is the ability for the menu to dynamically change as the items are selected and while the menu is still displayed (basically like a UIViewController).
Keeping the menu presented lets you select a menu item multiple times - so when done and the menu re-displayed, the state might be checked or unchecked depending on how many selects the user performed.
So this feature is strange as the visible menu's check marks will be inconsistent with the actual state until the UIMenu is re-created.
Upvotes: -1
Reputation: 9141
When using iOS 15 or newer, you can use UIDeferredMenuElement.uncached
to create dynamic UIAction
objects:
let menu = UIMenu(options: .displayInline, children: [
UIDeferredMenuElement.uncached { [weak self] completion in
let actions = [
UIAction(title: "My Dynamic Action 1", image: UIImage(systemName: "hourglass"), state: self?.myState1 == true ? .on : .off, handler: { (action) in
// Do something
}),
UIAction(title: "My Dynamic Action 2", image: UIImage(systemName: "star"), state: self?.myState2 == true ? .on : .off, handler: { (action) in
// Do something else
})
]
completion(actions)
}
])
Documentation: https://developer.apple.com/documentation/uikit/uideferredmenuelement/3857602-elementwithuncachedprovider
Upvotes: 6
Reputation: 3164
You will need to recreate the Menu. This example will also correctly select the item that was tapped on:
private func setupViews()
timeFrameButton = UIBarButtonItem(
image: UIImage(systemName: "calendar"),
menu: createMenu()
)
navigationItem.leftBarButtonItem = timeFrameButton
}
private func createMenu(actionTitle: String? = nil) -> UIMenu {
let menu = UIMenu(title: "Menu", children: [
UIAction(title: "Yesterday") { [unowned self] action in
self.timeFrameButton.menu = createMenu(actionTitle: action.title)
},
UIAction(title: "Last week") { [unowned self] action in
self.timeFrameButton.menu = createMenu(actionTitle: action.title)
},
UIAction(title: "Last month") { [unowned self] action in
self.timeFrameButton.menu = createMenu(actionTitle: action.title)
}
])
if let actionTitle = actionTitle {
menu.children.forEach { action in
guard let action = action as? UIAction else {
return
}
if action.title == actionTitle {
action.state = .on
}
}
} else {
let action = menu.children.first as? UIAction
action?.state = .on
}
return menu
}
Upvotes: 6
Reputation: 27427
Replace the entire UIMenu
object on state change would do the trick.
// view controller
internal var barButton: UIBarButtonItem!
// UI setup function
barButton = UIBarButtonItem(
image: UIImage(systemName: "arrow.up.arrow.down.square"),
primaryAction: nil,
menu: generatePullDownMenu()
)
// On state change inside UIAction
let actionNextSeen = UIAction(
title: "foo",
image: UIImage(systemName: "hourglass", )
state: someVariable ? .off : .on
) { _ in
someVariable = false
self.barButton.menu = self.generatePullDownMenu()
}
REFERENCE
https://developer.apple.com/forums/thread/653862
Upvotes: 19
Reputation: 535944
Do not attempt to change the menu while it is showing. Respond to the choice by changing your data. At the same time, the menu vanishes, because the user has chosen an action. But now you use that data to construct the menu the next time the user displays the menu.
Upvotes: 2