Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to modify UIMenu for UIBarButtonItem and UIButton

iOS 14 adds the ability to display menus upon tapping or long pressing a UIBarButtonItem or UIButton, like so:

let menu = UIMenu(children: [UIAction(title: "Action", image: nil) { action in
    //do something
}])
button.menu = menu
barButtonItem = UIBarButtonItem(title: "Show Menu", image: nil, primaryAction: nil, menu: menu)

This most often replaces action sheets (UIAlertController with actionSheet style). It's really common to have a dynamic action sheet where actions are only included or may be disabled based on some state at the time the user taps the button. But with this API, the menu is created at the time the button is created. How can you modify the menu prior to it being presented or otherwise make it dynamic to ensure the appropriate actions are available and in the proper state when it will appear?

like image 935
Jordan H Avatar asked Jul 18 '20 15:07

Jordan H


2 Answers

After some trial, I've found out that you can modify the UIButton 's .menu by setting the menu property to null first then set the new UIIMenu

here is the sample code that I made

@IBOutlet weak var button: UIButton!

func generateMenu(max: Int, isRandom: Bool = false) -> UIMenu {
    let n = isRandom ? Int.random(in: 1...max) : max
    print("GENERATED MENU: \(n)")
    let items = (0..<n).compactMap { i -> UIAction in
        UIAction(
            title: "Menu \(i)",
            image: nil
        ) {[weak self] _ in
            guard let self = self else { return }

            self.button.menu = nil // HERE

            self.button.menu = self.generateMenu(max: 10, isRandom: true)
            print("Tap")
        }
    }
 
    let m = UIMenu(
        title: "Test", image: nil,
        identifier: UIMenu.Identifier(rawValue: "Hello.menu"),
        options: .displayInline, children: items)
    return m
}

override func viewDidLoad() {
    super.viewDidLoad()
    button.menu = generateMenu(max: 10)
    button.showsMenuAsPrimaryAction = true
}
like image 172
Michael Henry Avatar answered Sep 20 '22 16:09

Michael Henry


You can store a reference to your bar button item or button and recreate the menu each time any state changes that affects the available actions in the menu. menu is a settable property so it can be changed any time after the button is created. You can also get the current menu and replace its children like so: button.menu = button.menu?.replacingChildren([])

For scenarios where you are not informed when the state changes for example, you really need to be able to update the menu right before it appears. There is a UIDeferredMenuElement API which allows the action(s) to be generated dynamically. It's a block where you call a completion handler providing an array of UIMenuElement. A placeholder with loading UI is added by the system and is replaced once you call the completion handler, so it supports asynchronous determination of menu items. However, this block is only called once and then it is cached and reused so this doesn't do what we need for this scenario. iOS 15 added a new uncached provider API which behaves the same way except the block is invoked every time the element is displayed, which is exactly what we need for this scenario.

barButtonItem.menu = UIMenu(children: [
    UIDeferredMenuElement.uncached { [weak self] completion in
        var actions = [UIMenuElement]()
        if self?.includeTestAction == true {
            actions.append(UIAction(title: "Test Action") { [weak self] action in
                self?.performTestAction()
            })
        }
        completion(actions)
    }
])

Before this API existed, I did find for UIButton you can change the menu when the user touches down via target/action like so: button.addTarget(self, action: #selector(buttonTouchedDown(_:)), for: .touchDown). This worked only if showsMenuAsPrimaryAction was false so they had to long press to open the menu. I didn't find a solution for UIBarButtonItem, but you could use a UIButton as a custom view.

like image 42
Jordan H Avatar answered Sep 17 '22 16:09

Jordan H