8

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.

JAL
  • 41,701
  • 23
  • 172
  • 300
cud_programmer
  • 1,244
  • 1
  • 20
  • 37
  • Possible duplicate of [UITextView: Disable selection, allow links](https://stackoverflow.com/questions/36198299/uitextview-disable-selection-allow-links) – Cœur Mar 23 '18 at 06:41
  • @cud_programmer can you please unacceptably my answer and accept Cœur's? My answer does not work anymore and is invalid. – JAL Dec 23 '19 at 16:22

6 Answers6

9

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)
            }
        }
    }
}
NiñoScript
  • 4,523
  • 2
  • 27
  • 33
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • This works great! Also take a look at this other answer for a related question that let's taps fall through instead making the text unselectable: https://stackoverflow.com/a/29737178/1904287 – NiñoScript Jan 11 '19 at 19:26
5

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 { }
}
Pablo Sanchez Gomez
  • 1,438
  • 16
  • 28
3

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

Evgeny Mitko
  • 245
  • 2
  • 12
  • 1
    Seems to work partially, but if you double tap but hold a bit on the second tap then you can still select text. Maybe if you adjusted the conditions. Using breakpoints there seem to be a number of different recognizers at play. – shim Jul 17 '17 at 19:35
  • 1
    @shim is right, but this is the best answer to the question - thanks! – Fraser Jul 26 '17 at 01:55
2

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

Max Chuquimia
  • 7,494
  • 2
  • 40
  • 59
  • 1
    This solution is brilliant. – Elardus Erasmus Aug 30 '17 at 18:54
  • If you tap FAR BELOW from the textview, `closestPosition(to: point)` is giving you the last character; if it is a link, then gestures are accepted: so double-tap and drag and selection will become possible. Gestures on the link itself are also a problem. – Cœur Mar 22 '18 at 13:54
1

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.

Eray Diler
  • 1,095
  • 1
  • 10
  • 17
-1

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.

JAL
  • 41,701
  • 23
  • 172
  • 300
  • 6
    This doesn't work. All of the text is still selectable – Alex Salom Feb 24 '17 at 08:28
  • Sorry @AlexSalom, I can't seem to reproduce that. Just tested this with a fresh Swift 3 project and I can't select the text. Are you sure you're overriding the correct method for the version of Swift you are using? There are differences between Swift 2 and 3. – JAL Feb 24 '17 at 15:30
  • Is it possible that my problem relates to the fact of UITextView being embedded into a UICollectionView? – Alex Salom Feb 28 '17 at 09:16
  • 1
    A long press still allows selection – user3352495 Jun 15 '17 at 20:36
  • @user3352495 what iOS version are you using? – JAL Jun 15 '17 at 20:53
  • @JAL Latest iOS, 10.3.2. My textView is embedded in a table view footer. – user3352495 Jun 16 '17 at 16:23
  • @user3352495 You may be having the same problem as AlexSalom has. I didn't test this with a UITextView as a subview of a reusable view. I'll have to investigate further. – JAL Jun 16 '17 at 16:31
  • My text view is not a subview of a reusable view, and this does not work. Note the text view is not editable. – shim Jul 17 '17 at 19:41
  • Doesn't work at least in iOS 11.2 simulator. The method `becomeFirstResponder()` is being called (verified in the debugger) but the text can still be selected. – petrsyn Dec 15 '17 at 00:03
  • Still selection indicator is coming – Abhishek Thapliyal Jun 06 '18 at 08:42
  • this doesn't work. the text is still selectable inside textview – Ashim Dahal Jan 27 '20 at 22:47
  • @AshimDahal this answer is no longer valid, please use [Cœur's answer below](https://stackoverflow.com/a/49428307/2415822). Unfortunately this answer is stuck at the top because it is accepted. – JAL Jan 27 '20 at 22:55