cud_programmer
cud_programmer

Reputation: 1264

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.

Upvotes: 10

Views: 8688

Answers (6)

JAL
JAL

Reputation: 42449

This answer is for iOS 10.3.x and below where your UIView is not embedded in a subview. For a more robust, modern answer, please see Cœur's answer below.

You need to prevent the UITextView from becoming first responder.

1. Subclass UITextView to your own custom class (MyTextView).

2. Override canBecomeFirstResponder(). Here's an example in Swift:

Swift 3:

class MyTextView: UITextView {
    override func becomeFirstResponder() -> Bool {
        return false
    }
}

Swift 2:

class MyTextView: UITextView {
    override func canBecomeFirstResponder() -> Bool {
        return false
    }
}

Any links detected will still be enabled. I tested this with a phone number.

Upvotes: -1

Cœur
Cœur

Reputation: 38667

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

Upvotes: 9

Pablo Sanchez Gomez
Pablo Sanchez Gomez

Reputation: 1508

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

Upvotes: 5

Eray Diler
Eray Diler

Reputation: 1145

This is what worked for me;

class LinkDetectingTextView: UITextView {
    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if isEditable == false {
            if let _ = gestureRecognizer as? UITapGestureRecognizer {
                return false
            }

            if let longPressRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
                longPressRecognizer.minimumPressDuration == 0.5 { // prevent to select text but allow certain functionality in application

                return false
            }
        }

        return true
    }
}

In addition, set minimumPressDuration of the longPressGestureRecognizer in the application another value different than 0.5.

Upvotes: 1

Evgeny Mitko
Evgeny Mitko

Reputation: 285

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 🤷‍♂️

Upvotes: 3

Max Chuquimia
Max Chuquimia

Reputation: 7824

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

Upvotes: 2

Related Questions