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