Get an event when UIBarButtonItem menu is displayed
Asked Answered
A

2

4

We all know how to make a simple tap on a bar button item present a menu (introduced on iOS 14):

    let act = UIAction(title: "Howdy") { act in
        print("Howdy")
    }
    let menu = UIMenu(title: "", children: [act])
    self.bbi.menu = menu // self.bbi is the bar button item

So far, so good. But presenting the menu isn't the only thing I want to do when the bar button item is tapped. As long as the menu is showing, I need to pause my game timers, and so on. So I need to get an event telling me that the button has been tapped.

I don't want this tap event to be different from the producing of the menu; for example, I don't want to attach a target and action to my button, because if I do that, then the menu production is a different thing that happens only when the user long presses on the button. I want the menu to appear on the tap, and receive an event telling me that this is happening.

This must be a common issue, so how are people solving it?

Agnosia answered 25/10, 2021 at 14:56 Comment(0)
B
4

The only way I could find was to use UIDeferredMenuElement to perform something on a tap of the menu. However, the problem is that you have to recreate the entire menu and assign it to the bar button item again inside the deferred menu element's elementProvider block in order to get future tap events, as you can see in this toy example here:

class YourViewController: UIViewController {
    
    func menu(for barButtonItem: UIBarButtonItem) -> UIMenu {
        UIMenu(title: "Some Menu", children: [UIDeferredMenuElement { [weak self, weak barButtonItem] completion in
            guard let self = self, let barButtonItem = barButtonItem else { return }
            print("Menu shown - pause your game timers and such here")
            
            // Create your menu's real items here:
            let realMenuElements = [UIAction(title: "Some Action") { _ in
                print("Menu action fired")
            }]
            
            // Hand your real menu elements back to the deferred menu element
            completion(realMenuElements)
            
            // Recreate the menu. This is necessary in order to get this block to
            // fire again on future taps of the bar button item.
            barButtonItem.menu = self.menu(for: barButtonItem)
        }])
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let someBarButtonItem = UIBarButtonItem(systemItem: .done)
        someBarButtonItem.menu = menu(for: someBarButtonItem)
        navigationItem.rightBarButtonItem = someBarButtonItem
    }
}

Also, it looks like starting in iOS 15 there's a class method on UIDeferredMenuElement called uncached(_:) that creates a deferred menu element that fires its elementProvider block every time the bar button item is tapped instead of just the first time, which would mean you would not have to recreate the menu as in the example above.

Bingen answered 26/10, 2021 at 2:49 Comment(1)
Ingenious as usual. I'll have to look into this.Agnosia
S
0

You can do this using UIControl.Event.menuActionTriggered. Add a menu as the button's primary action, but also add a handler for this event.

You'll still get the "tap to display menu" behavior, but your handler will also get called.

(It would be great if there were a way to find out when the menu has been dismissed, too, but there doesn't seem to be).

Signalman answered 21/5 at 16:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.