Does UITextInteraction have a way to dismiss the edit menu?

If I use UIEditMenuInteraction to present an edit menu, it has a dismissMenu method that I can call to remove the menu when necessary.

When I use UITextInteraction, I get an edit menu automatically that is normally presented and dismissed at appropriate times. But sometimes I want to dismiss the menu myself, and I can't find a way to do that. Am I missing something? I was hoping to find that UITextInteraction inherited from UIEditMenuInteraction, or had some other way to access the underlying menu in order to dismiss it. But it seems that the menu must be a private part of the UITextInteraction implementation.

The particular case that I need to deal with is when I call resignFirstResponder. This seems to cause the keyboard to close and the insertion point and any selection to be hidden, but if an edit menu was shown then it remains visible (a ghost!). If anyone knows of an alternative to resignFirstResponder that will make UITextInteraction tidy up properly, that would also be useful to know.

Thanks for any suggestions!

Replies

Answering my own question....

When you add a UITextInteraction, a UIEditMenuInteraction gets added automatically. So it's possible to iterate through the view's interactions and find the menu interaction using isKindOfClass, and call dismissMenu on that.

It's a bit of a hack, but I've done much worse and it seems to work.

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)
                }
            }
        }
    }
}