Context
I’m currently developing a chat-based app that requires the implementation of coping messages. Essentially, when a user long-presses a message bubble, an edit menu will appear with the option to “Copy.” This feature is a common element across most chat applications.
For the purpose of this article, let’s assume that all message bubbles are constructed using a subclass of UITextView called MessageTextView. To make the “Copy” option appear on a long-press gesture, we’ll first need to add the appropriate gesture recognizer:
let textView = MessageTextView()
let longPressGesture = UILongPressGestureRecognizer(
target: self,
action: #selector(handleLongPressGesture(recognizer:)))
textView.addGestureRecognizer(longPressGesture)
@objc
func handleLongPressGesture(recognizer: UIGestureRecognizer) {}
Next, we’ll move on to the second step, which involves implementing the handleLongPressGesture method:
// 1
guard recognizer.state == .began,
let recognizerView = recognizer.view,
let superview = recognizerView.superview else { return }
// 2
textView?.becomeFirstResponder()
// 3
UIMenuController.shared.showMenu(from: superview, rect: recognizerView.frame)
Let’s now break down the steps necessary to implement this feature:
- First, we need to obtain the recognizerView and its parent view from the recognizer. We’ll need both of these to properly position the edit menu on the screen.
- Next, we’ll make the edit menu the first responder to ensure that it appears on the screen.
- Finally, we’ll show the menu.
Note that at this point, all available options will be displayed, so we still need to filter out any unnecessary edit menu options. To do this, we’ll need to override two methods in our MessageTextView:
override public func canPerformAction(
_ action: Selector,
withSender sender: Any?) -> Bool {
// 1
action == #selector(copy(_:))
}
@objc
override public func copy(_ sender: Any?) {
// 2
UIPasteboard.general.string = text
}
- We filter out all other options, except for “copy”.
- As per Apple’s documentation:
UIKit calls this method when the user selects the Copy command from an editing menu. Your implementation should write the selected content to the pasteboard without removing the selection from your interface.
At first glance, it may seem like we’ve completed the implementation of this feature. However, there’s one more detail that we need to address. Currently, if the keyboard is visible on the screen, the edit menu will be displayed in the wrong location. This occurs because we’re calling showMenu(from:rect:)
while the keyboard is still visible. Once the keyboard hides, the layout of the conversation view (i.e. the screen where all the messages are displayed) will likely change.
Handling keyboard events
One solution to present the edit menu without hiding the keyboard is by using the UIResponder.next
property, as described in this article. However, in my case, this is not feasible due to the modular architecture of the app I’m working on. The message composer (the view where the user composes a message) and conversation view (the view where all the message bubbles are displayed) are separate modules, and iOS doesn’t provide an easy way to obtain a reference to the current first responder.
Therefore, I found that the best solution for this scenario is to listen for the UIResponder.keyboardDidHideNotification
and update the edit menu accordingly. To implement this in MessageTextView
:
// Text view subclass
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardDidHideNotification(notification:)),
name: UIResponder.keyboardDidHideNotification,
object: nil)
@objc
fileprivate func handleKeyboardDidHideNotification(notification: Notification) {
UIMenuController.shared.update()
}
Conclusion
That’s it! The edit menu will update its position when keyboard hides.
Notes
UIMenuController
has been deprecated in iOS 16. See UIEditMenuInteraction.- Remember to check out Using the UIMenuController and Manipulating the Responder Chain