Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect Data in UITextView in UITableViewCell

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

like image 898
adamF Avatar asked Dec 10 '13 23:12

adamF


1 Answers

1. Using 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
    }

}

enter image description here


2. Using 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".

like image 160
Imanou Petit Avatar answered Sep 22 '22 01:09

Imanou Petit