I created an extension to UITextInput
that adds methods to let you show/hide the menu. This assumes the custom UITextInput
is making use of UITextInteraction
.
The following works for iOS 15+ (tested with iOS 15.4, 16.4, 17.4, and 17.5). This has only been tested in development. It arguably uses a private API so I don't know (yet) if this will be approved for App Store apps.
extension UITextInput {
@available(iOS 16.0, *)
var editMenuInteraction: UIEditMenuInteraction? {
return textInputView?.interactions.first { $0 is UIEditMenuInteraction } as? UIEditMenuInteraction
}
func showEditMenu() {
if let textInputView {
if #available(iOS 16.0, *) {
// There's no direct API to show the menu. Normally you setup a UIEditMenuInteraction but the
// UITextInteraction sets up its own. So we need to find that interaction and call its
// presentEditMenu with a specially crafted UIEditMenuConfiguration.
if let interaction = self.editMenuInteraction {
if let selectedRange = self.selectedTextRange {
let rect = self.firstRect(for: selectedRange)
if !rect.isNull {
let pt = CGPoint(x: rect.midX, y: rect.minY - textInputView.frame.origin.y)
// !!!: Possible future failure
// This magic string comes from:
// -[UITextContextMenuInteraction _querySelectionCommandsForConfiguration:suggestedActions:completionHandler:]
// It can be seen by looking at the assembly for the _querySelectionCommandsForConfiguration method.
// About 24 lines down is a reference to this string literal.
let cfg = UIEditMenuConfiguration(identifier: "UITextContextMenuInteraction.TextSelectionMenu", sourcePoint: pt)
interaction.presentEditMenu(with: cfg)
}
}
}
} else {
if let selectedRange = self.selectedTextRange {
let rect = self.firstRect(for: selectedRange)
if !rect.isNull {
UIMenuController.shared.showMenu(from: textInputView, rect: rect.insetBy(dx: 0, dy: -textInputView.frame.origin.y))
}
}
}
}
}
func hideEditMenu() {
if let textInputView {
if #available(iOS 16.0, *) {
if let interaction = self.editMenuInteraction {
interaction.dismissMenu()
}
} else {
if UIMenuController.shared.isMenuVisible {
UIMenuController.shared.hideMenu(from: textInputView)
}
}
}
}
}