Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITextView link selectable without rest of text being selectable

I'm trying to get a setup similar to what Facebook use (if they use a UITextView). I want links to be detected automatically however I don't want any other text in the UITextView selectable. So, the user can click on the link but is unable to select any other text.

Despite searching around, I've yet to come across a solution as for link selection to work it requires the whole of the text view to be selectable.

like image 815
cud_programmer Avatar asked Jan 16 '15 16:01

cud_programmer


4 Answers

if your minimum deployment target is iOS 11.2 or newer

You can disable text selection by subclassing UITextView and forbidding the gestures that can select something.

The below solution is:

  • compatible with isEditable
  • compatible with isScrollEnabled
  • compatible with links
/// Class to allow links but no selection.
/// Basically, it disables unwanted UIGestureRecognizer from UITextView.
/// https://stackoverflow.com/a/49428307/1033581
class UnselectableTappableTextView: UITextView {

    // required to prevent blue background selection from any situation
    override var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer is UIPanGestureRecognizer {
            // required for compatibility with isScrollEnabled
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
            tapGestureRecognizer.numberOfTapsRequired == 1 {
            // required for compatibility with links
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // allowing smallDelayRecognizer for links
        // https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
        if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
            // comparison value is used to distinguish between 0.12 (smallDelayRecognizer) and 0.5 (textSelectionForce and textLoupe)
            longPressGestureRecognizer.minimumPressDuration < 0.325 {
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // preventing selection from loupe/magnifier (_UITextSelectionForceGesture), multi tap, tap and a half, etc.
        gestureRecognizer.isEnabled = false
        return false
    }
}

if your minimum deployment target is iOS 11.1 or older

Native UITextView links gesture recognizers are broken on iOS 11.0-11.1 and require a small delay long press instead of a tap: Xcode 9 UITextView links no longer clickable

You can properly support links with your own gesture recognizer and you can disable text selection by subclassing UITextView and forbidding the gestures that can select something or tap something.

The below solution will disallow selection and is:

  • compatible with isScrollEnabled
  • compatible with links
  • workaround limitations of iOS 11.0 and iOS 11.1, but loses the UI effect when tapping on text attachments
/// Class to support links and to disallow selection.
/// It disables most UIGestureRecognizer from UITextView and adds a UITapGestureRecognizer.
/// https://stackoverflow.com/a/49428307/1033581
class UnselectableTappableTextView: UITextView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        // Native UITextView links gesture recognizers are broken on iOS 11.0-11.1:
        // https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
        // So we add our own UITapGestureRecognizer.
        linkGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped))
        linkGestureRecognizer.numberOfTapsRequired = 1
        addGestureRecognizer(linkGestureRecognizer)
        linkGestureRecognizer.isEnabled = true
    }

    var linkGestureRecognizer: UITapGestureRecognizer!

    // required to prevent blue background selection from any situation
    override var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
        // Prevents drag and drop gestures,
        // but also prevents a crash with links on iOS 11.0 and 11.1.
        // https://stackoverflow.com/a/49535011/1033581
        gestureRecognizer.isEnabled = false
        super.addGestureRecognizer(gestureRecognizer)
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer == linkGestureRecognizer {
            // Supporting links correctly.
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        if gestureRecognizer is UIPanGestureRecognizer {
            // Compatibility support with isScrollEnabled.
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // Preventing selection gestures and disabling broken links support.
        gestureRecognizer.isEnabled = false
        return false
    }

    @objc func textTapped(recognizer: UITapGestureRecognizer) {
        guard recognizer == linkGestureRecognizer else {
            return
        }
        var location = recognizer.location(in: self)
        location.x -= textContainerInset.left
        location.y -= textContainerInset.top
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let characterRange = NSRange(location: characterIndex, length: 1)

        if let attachment = attributedText?.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment {
            if #available(iOS 10.0, *) {
                _ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange, interaction: .invokeDefaultAction)
            } else {
                _ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange)
            }
        }
        if let url = attributedText?.attribute(.link, at: characterIndex, effectiveRange: nil) as? URL {
            if #available(iOS 10.0, *) {
                _ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange, interaction: .invokeDefaultAction)
            } else {
                _ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange)
            }
        }
    }
}
like image 194
Cœur Avatar answered Nov 05 '22 07:11

Cœur


You can subclass the UITextView overriding the method of selectedTextRange, setting it to nil. And the links will still be clickable, but you won't be able to select the rest of the text (even the link but you can click on it).

class CustomTextView: UITextView {
override public var selectedTextRange: UITextRange? {
    get {
        return nil
    }
    set { }
}
like image 35
Pablo Sanchez Gomez Avatar answered Nov 05 '22 08:11

Pablo Sanchez Gomez


You need to subclass UITextView and override gestureRecognizerShouldBegin (_:) method like this:

override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    if isEditable == false {
        if let gesture =  gestureRecognizer as? UILongPressGestureRecognizer, gesture.minimumPressDuration == 0.5 {
            return false
        }
    }
    return true
}

this will prevent from textview being selected but link will work as expected

Edited: It turned out that when double tap and hold you are still able to select text. As I figured out it happens after two taps(not the UITapGesture with property "minimalNumberOfTaps", but to different taps one after another), so the solution is to track time after first step (approx. 0.7 sec) Full code:

var lastTapTime: TimeInterval = 0
    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if isEditable == false {
            if let gesture =  gestureRecognizer as? UILongPressGestureRecognizer, gesture.minimumPressDuration == 0.5 {
        return false
            }
        }
        if Date().timeIntervalSince1970 >= lastTapTime + 0.7 {
            lastTapTime = Date().timeIntervalSince1970
            return true
        } else {
            return false
        }
    }

This is not the most elegant solution but it seems to work 🤷‍♂️

like image 40
Evgeny Mitko Avatar answered Nov 05 '22 09:11

Evgeny Mitko


The selected answer doesn't work in my case, and I'm not comfortable with comparing unconfirmed values inside of internal UIGestureRecognizers.

My solution was to override point(inside:with:) and allow a tap-through when the user is not touching down on linked text: https://stackoverflow.com/a/44878203/1153630

like image 20
Max Chuquimia Avatar answered Nov 05 '22 07:11

Max Chuquimia