Reputation: 1264
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
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
Reputation: 38667
You can disable text selection by subclassing UITextView
and forbidding the gestures that can select something.
The below solution is:
/// 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
}
}
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:
/// 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
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
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
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
Reputation: 7824
The selected answer doesn't work in my case, and I'm not comfortable with comparing unconfirmed values inside of internal UIGestureRecognizer
s.
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