iPadOS: Prevent UIContextMenuInteraction from triggering when not using pointer

858 Views Asked by At

UIMenu vs UIContextMenuInteraction vs UIPointerInteraction

I'm trying to set up UIContextMenuInteraction in the same way as in Files or Pages app:

  • (Long) tap anywhere in the blank space shows the black horizontal UIMenu
  • Secondary (Right/Control) click with a pointer anywhere in the blank space shows context menu

See demo on the attached GIF below.

I'm able to set up UIContextMenuInteraction and in its UIContextMenuInteractionDelegate return the UIContextMenuConfiguration with items I want to show.

The same for the small black UIMenu, I could use UILongPressGestureRecognizer and show the menu using UIMenuController.shared.showMenu.

However, I'm not able to prevent UIContextMenuInteraction from triggering and showing the UITargetedPreview when long-pressing on a view and there seems to be now way of recognizing different UITouchTypes with the info provided to UIContextMenuInteractionDelegate.

I also could not find how to show the context menu programatically, without UIContextMenuInteraction. Is there a way to do that?

Question

How is this implemented in Files.app?

Files.app ui menu and context menu interaction

1

There are 1 best solutions below

0
On

There is no way to programmatically trigger a context menu, but with some simple bookkeeping you can prevent it from showing when not wanted (e.g when touches are active on your responder).

To hide the preview, just return nil from previewProvider in UIContextMenuConfiguration's initializer.

Here is a complete implementation with a view controller's view as the target:

import UIKit

class ViewController: UIViewController {

    var touchesInSession = false

    override func viewDidLoad() {
        super.viewDidLoad()

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

        let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler))
        view.addGestureRecognizer(recognizer)
    }

    @objc func longPressHandler(recognizer: UILongPressGestureRecognizer) {
        guard recognizer.state == .began else { return }
        presentMenu(from: recognizer.location(in: view))
    }

    func presentMenu(from location: CGPoint) {
        view.becomeFirstResponder()
        let saveMenuItem = UIMenuItem(title: "New Folder", action: #selector(createFolder))
        let deleteMenuItem = UIMenuItem(title: "Get Info", action: #selector(getInfo))
        UIMenuController.shared.menuItems = [saveMenuItem, deleteMenuItem]
        UIMenuController.shared.showMenu(from: view, rect: .init(origin: location, size: .zero))
    }

    @objc func createFolder() {
        print("createFolder")
    }

    @objc func getInfo() {
        print("getInfo")
    }

    // MARK: UIResponder
    override var canBecomeFirstResponder: Bool { true }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        touchesInSession = true
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        touchesInSession = false
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event)
        touchesInSession = false
    }
}

extension ViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        guard !touchesInSession else { return nil }
        let configuration = UIContextMenuConfiguration(identifier: "PointOnlyContextMenu" as NSCopying, previewProvider: { nil }, actionProvider: { suggestedActions in
            let newFolder = UIAction(title: "New Folder", image: UIImage(systemName: "folder.badge.plus")) { [weak self] _ in
                self?.createFolder()
            }
            let info = UIAction(title: "Get Info", image: UIImage(systemName: "info.circle")) { [weak self] _ in
                self?.getInfo()
            }
            return UIMenu(title: "", children: [newFolder, info])
        })
        return configuration
    }
}