XYZ
XYZ

Reputation: 27427

How to change the state for an UIAction inside UIMenu at runtime?

How to change the state for an UIAction? The goal is to toggle a state checkmark next to an UIAction inside UIMenu.

enter image description here

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

Answers (5)

KonaCurrents
KonaCurrents

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

Ely
Ely

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

Jos&#233;
Jos&#233;

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

XYZ
XYZ

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

matt
matt

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

Related Questions