Context menu under touch

I'm building an iOS based app that has a RealityKit view filling the screen.

I would like to present a context menu at the location of my tap, so that I can present context sensitive controls for the piece of geometry I'm tapping on.

I managed to setup my context menu similar to this:

class MyARView: ARView, UIContextMenuInteractionDelegate {
	required init(frame frameRect: CGRect) {
		super.init(frame: frameRect)
		setupContextMenu()
	}
	
	required dynamic init?(coder decoder: NSCoder) {
		super.init(coder: decoder)
		setupContextMenu()
	}
	
	override init(frame frameRect: CGRect, cameraMode: ARView.CameraMode, automaticallyConfigureSession: Bool) {
		super.init(frame: frameRect, cameraMode: cameraMode, automaticallyConfigureSession: automaticallyConfigureSession)
		setupContextMenu()
	}
	
	func isValidHitTest(at location: CGPoint) -> Bool {
		// TODO: Implement...
	}
	
	func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
		guard isValidHitTest(at: location) else {
			return nil
		}
	
		UIContextMenuConfiguration(
			identifier: nil,
			previewProvider: nil,
			actionProvider: { _ in
				let action = UIAction(title: "Menu Option") { _ in
					print("Do the thing")
				}
				return UIMenu(title: "", children: [action])
			}
		)
	}
	
	func setupContextMenu() {
		addInteraction(UIContextMenuInteraction(delegate: self))
	}
}

This works technically, but it doesn't provide the behaviour I'd expect. If I tap on the view (anywhere), the context menu appears at the top right of the view, but never at the location of the tap. If I use a mouse with the iPad, and right click, it always appears under the mouse.

Is there any way I can configure a menu that will appear under the tap?

I don't mind if it's using UIContextMenuInteraction, and would actually prefer the visual style of UIEditMenuInteraction on iPad. But generally, as long as I can get a list of options to appear in a menu under the mouse, that's fine.

Answered by Matt Cox in 768509022

I managed to get the edit menu working with the following code.

class MyARView: ARView, UIEditMenuInteractionDelegate {
	private var editMenuInteraction: UIEditMenuInteraction? = nil

	required init(frame frameRect: CGRect) {
		super.init(frame: frameRect)
		setupEditMenu()
	}
	
	required dynamic init?(coder decoder: NSCoder) {
		super.init(coder: decoder)
		setupEditMenu()
	}
	
	override init(frame frameRect: CGRect, cameraMode: ARView.CameraMode, automaticallyConfigureSession: Bool) {
		super.init(frame: frameRect, cameraMode: cameraMode, automaticallyConfigureSession: automaticallyConfigureSession)
		setupEditMenu()
	}
	
	func isValidHitTest(at location: CGPoint) -> Bool {
		// TODO: Implement...
		//
		return true
	}
	
	func setupEditMenu() {
		print("Hello, world!")
		
		editMenuInteraction = UIEditMenuInteraction(delegate: self)
		if let editMenuInteraction {
			self.addInteraction(editMenuInteraction)
		}
		
		let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
		self.addGestureRecognizer(longPressGestureRecognizer)
	}
	
	func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
		UIMenu(
			title: "",
			options: .displayInline,
			children: [
				UIAction(title: "Menu Option") { _ in
					print("Do the thing")
				}
			]
		)
    }
	
	@objc
	func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
		guard gestureRecognizer.state == .began else {
			return
		}
		
		let location = gestureRecognizer.location(in: self)
		guard isValidHitTest(at: location) else {
			return
		}
		
		let configuration = UIEditMenuConfiguration(identifier: "MyARView", sourcePoint: location)
		editMenuInteraction?.presentEditMenu(with: configuration)
	}
}
Accepted Answer

I managed to get the edit menu working with the following code.

class MyARView: ARView, UIEditMenuInteractionDelegate {
	private var editMenuInteraction: UIEditMenuInteraction? = nil

	required init(frame frameRect: CGRect) {
		super.init(frame: frameRect)
		setupEditMenu()
	}
	
	required dynamic init?(coder decoder: NSCoder) {
		super.init(coder: decoder)
		setupEditMenu()
	}
	
	override init(frame frameRect: CGRect, cameraMode: ARView.CameraMode, automaticallyConfigureSession: Bool) {
		super.init(frame: frameRect, cameraMode: cameraMode, automaticallyConfigureSession: automaticallyConfigureSession)
		setupEditMenu()
	}
	
	func isValidHitTest(at location: CGPoint) -> Bool {
		// TODO: Implement...
		//
		return true
	}
	
	func setupEditMenu() {
		print("Hello, world!")
		
		editMenuInteraction = UIEditMenuInteraction(delegate: self)
		if let editMenuInteraction {
			self.addInteraction(editMenuInteraction)
		}
		
		let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
		self.addGestureRecognizer(longPressGestureRecognizer)
	}
	
	func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
		UIMenu(
			title: "",
			options: .displayInline,
			children: [
				UIAction(title: "Menu Option") { _ in
					print("Do the thing")
				}
			]
		)
    }
	
	@objc
	func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
		guard gestureRecognizer.state == .began else {
			return
		}
		
		let location = gestureRecognizer.location(in: self)
		guard isValidHitTest(at: location) else {
			return
		}
		
		let configuration = UIEditMenuConfiguration(identifier: "MyARView", sourcePoint: location)
		editMenuInteraction?.presentEditMenu(with: configuration)
	}
}
Context menu under touch
 
 
Q