Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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])
        }
    }
}
like image 882
mmklug Avatar asked Sep 01 '19 09:09

mmklug


3 Answers

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
}

like image 175
mmklug Avatar answered Nov 07 '22 18:11

mmklug


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.

like image 8
Hejazi Avatar answered Nov 07 '22 17:11

Hejazi


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)
}
like image 4
Lane Avatar answered Nov 07 '22 17:11

Lane