What is the best way to detect when user taps on NSTextAttachment on iOS?
I think that one of the ways would be checking for the character on carret\'s
I have altered Drew's gesture recognizer here to subclass UITapGestureRecognizer rather than UIGestureRecognizer.
This offers one advantage in that it only detects discrete taps as opposed to the beginning of a scroll.
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Modified from: https://stackoverflow.com/a/49153247/658604
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
private(set) var tappedState: TappedAttachment?
override func touchesBegan(_ touches: Set, with event: UIEvent) {
tappedState = nil
guard let textView = view as? UITextView else {
state = .failed
return
}
if let touch = touches.first {
tappedState = evaluateTouch(touch, on: textView)
}
if tappedState != nil {
// UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
// Therefore, let the super view evaluate the correct state.
super.touchesBegan(touches, with: event)
} else {
// User didn't initiate a touch (tap or otherwise) on an attachment.
// Force the gesture to fail.
state = .failed
}
}
/// Tests to see if the user has tapped on a text attachment in the target text view.
private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
let point = touch.location(in: textView)
let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
guard let characterIndex = index, characterIndex < textView.textStorage.length else {
return nil
}
guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
return nil
}
guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
return nil
}
return (attachment, characterIndex)
}
}