I have a UITextView
inside a UITableViewCell
subclass. It has editable
and userInteractionEnabled
both set to NO
. This allows the UITextView
to detect data such as links and phone numbers. The data is detected correctly, but because userInteraction is disabled, it cannot respond to taps on that data. If I set userInteractionEnabled
to YES
, it works fine, but then the UITableViewCell
cannot be selected since the UITextView swallows the touch.
I want to follow the link if the user taps on it, but I want didSelectRowAtIndexPath:
to be called if the tap is on basic text.
I think the right approach is to subclass UITextView
and pass touches to the cell, but I can't seem to find a way to detect whether or not the tap was on a link.
This is a similar question, but the answer will just pass all touches to the cell. I want to only pass the touches if they are NOT on a piece of detected data. issue enabling dataDetectorTypes on a UITextView in a UITableViewCell
hitTest(_:with:)
UIView
has a method called hitTest(_:with:)
that has the following definition:
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
UITextView
being a subclass of UIView
, you can implement this method in any subclass of UITextView
you may want to create.
The following Swift 3 example shows a UITableViewController
that contains a single static UITableViewCell
. The UITableViewCell
embeds a UITextView
. Any tap on a link inside the UITextView
will launch Safari app; any tap on basic text inside the UITextView
will trigger a push segue to the following UIViewController
.
LinkTextView.swift
import UIKit
class LinkTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
isScrollEnabled = false
isEditable = false
isUserInteractionEnabled = true
isSelectable = true
dataDetectorTypes = .link
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Get the character index from the tap location
let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if we detect a link, handle the tap by returning self...
if let _ = textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) {
return self
}
// ... otherwise return nil ; the tap will go on to the next receiver
return nil
}
}
TextViewCell.swift
import UIKit
class TextViewCell: UITableViewCell {
@IBOutlet weak var textView: LinkTextView!
}
TableViewController.swift
import UIKit
class TableViewController: UITableViewController {
@IBOutlet weak var cell: TextViewCell!
override func viewDidLoad() {
super.viewDidLoad()
// Add text to cell's textView
let text = "http://www.google.com Lorem ipsum dolor sit amet, consectetur adipiscing elit, http://www.yahoo.com sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
cell.textView.text = text
}
}
point(inside:with:)
As an alternative to override hitTest(_:with:)
, you can use point(inside:with:)
. point(inside:with:)
has the following declaration:
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
Returns a Boolean value indicating whether the receiver contains the specified point.
The following code shows how to implement point(inside:with:)
instead of hitTest(_:with:)
in your UITextView
subclass:
LinkTextView.swift
import UIKit
class LinkTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
isScrollEnabled = false
isEditable = false
isUserInteractionEnabled = true
isSelectable = true
dataDetectorTypes = .link
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Get the character index from the tap location
let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if we detect a link, handle the tap by returning true...
if let _ = textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) {
return true
}
// ... otherwise return false ; the tap will go on to the next receiver
return false
}
}
The complete project is available on this GitHub repo: LinkTextViewCell.
You can learn more about managing a UITextView
inside a UITableViewCell
by reading Swifty Approach to Handling UITextViews With Links in Cells.
You can learn more about the difference between hitTest(_:with:)
and point(inside:with:)
by reading Apple's Guide and Sample Code: "Event Delivery: The Responder Chain".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With