'How to trigger UIContextMenuInteraction context menu programmatically?

I have set up an UIButton as the rightBarButtonItem in an UIViewController inside an UINavigationController and associated an iOS13 context menu to it.

Long pressing the button shows the context menu as expected.

Is there a way to show the context menu also by tapping on the button (e.g. by adding a target for the .touchUpInside event)?

The button/barButtonItem is set up as follows:

let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "plus"), for: .normal)

let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton

let interaction = UIContextMenuInteraction(delegate: self)
button.addInteraction(interaction)

The context menu is defined as follows:

extension ViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in
            let importAction = UIAction(title: "Import", image: UIImage(systemName: "folder")) { action in }
            let createAction = UIAction(title: "Create", image: UIImage(systemName: "square.and.pencil")) { action in }
            return UIMenu(title: "", children: [importAction, createAction])
        }
    }
}


Solution 1:[1]

The context menu is, by design, automatically shown by the system when an appropriate gesture (a force touch or a long press) occurs. You can't manually show it.

From docs:

A context menu interaction object tracks Force Touch gestures on devices that support 3D Touch, and long-press gestures on devices that don't support it.

UIKit manages all menu-related interactions and reports the selected action, if any, back to your app.

UPDATE:

Although it's still not possible to manually show the context menu in iOS 14, it's now possible to show the UIMenu we create for the context menu as a pull-down menu. Check @Lobo's answer for how to do that on iOS 14.

Solution 2:[2]

iOS 14

iOS 14 (first Beta) now supports the desired functionality. With the following code tapping on the UIBarButtonItem will display the menu immediately (also avoiding the blurred background that results from calling UIContextMenuInteraction):

override func viewDidLoad() {
    super.viewDidLoad()
    
    let importAction = UIAction(title: "Import", image: UIImage(systemName: "folder")) { action in }
    let createAction = UIAction(title: "Create", image: UIImage(systemName: "square.and.pencil")) { action in }
    
    let menuBarButton = UIBarButtonItem(
        title: "Add",
        image: UIImage(systemName:"plus"),
        primaryAction: nil,
        menu: UIMenu(title: "", children: [importAction, createAction])
    )
    
    self.navigationItem.rightBarButtonItem = menuBarButton
}

The functionality is achieved by not providing the primaryAction.

You can achieve the same effect using an UIButton. In that case you will need to set

button.showsMenuAsPrimaryAction = true

The full code for an UIButton could look like this:

override func viewDidLoad() {
    super.viewDidLoad()
   
    let button = UIButton(type: .system)
    button.setImage(UIImage(systemName: "plus"), for: .normal)
    button.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(button)
    
    NSLayoutConstraint.activate([
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])
    
    let importAction = UIAction(title: "Import", image: UIImage(systemName: "folder")) { action in }
    let createAction = UIAction(title: "Create", image: UIImage(systemName: "square.and.pencil")) { action in }
    
    let items = [importAction, createAction]
    
    button.menu = UIMenu(title: "Add", children: items)
    button.showsMenuAsPrimaryAction = true
}

Solution 3:[3]

Using a private API can do this job.

@objc
func buttonTapped() {
    // _presentMenuAtLocation:
    guard let interaction = imageView.interactions.first,
          let data = Data(base64Encoded: "X3ByZXNlbnRNZW51QXRMb2NhdGlvbjo="),
          let str = String(data: data, encoding: .utf8)
    else {
        return
    }
    let selector = NSSelectorFromString(str)
    guard interaction.responds(to: selector) else {
        return
    }
    interaction.perform(selector, with: CGPoint.zero)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 matt
Solution 2 mmklug
Solution 3 Lane