Detect Data in UITextView in UITableViewCell
Asked Answered
P

2

6

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

Precatory answered 10/12, 2013 at 23:37 Comment(5)
willSelectRowAtIndexPathHeidy
Can you try txtView.selectable = NO; or userInteration set yes and use UITapGestureRecognizer???Widescreen
Don't you think if the UITextView is editable and links selectable, the user might find it a bit hard to edit the links ?Drawing
One possible (and complex) solution would be to use what I do in this question, by disabling link detection, and using tappable UIViews instead. (again, complicated). Maybe someone else has a better solution.Drawing
Possible duplicate of issue enabling dataDetectorTypes on a UITextView in a UITableViewCellUpstate
I
11

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

Izawa answered 26/1, 2017 at 23:1 Comment(3)
Thanks for the detailed answer. It works great. I modified it slightly to handle the case where layoutManager chooses the closest character to the point and a link is the last character. let glyphIndex = layoutManager.glyphIndex(for: point, in: textContainer) let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: nil) if point.x > lineRect.maxX { return nil }Precatory
This answer is good. Just the link tap area is small for the font size.Wegner
So I add more "link at point" detection around the tap point.Wegner
M
3

I had the same issue. I solved it by subclassing UITextView and add a protocol :

@protocol SOTextViewDelegate;

@interface SOTextView : UITextView

@property (nonatomic, weak) id<SOTextViewDelegate> soDelegate;

@end

@protocol SOTextViewDelegate <NSObject>
@optional
- (void)soTextViewWasTapped:(SOTextView *)soTextview;

@end

In the implementation I've just added this :

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    if (self.selectedRange.length == 0 &&
        [_soDelegate respondsToSelector:@selector(soTextViewWasTapped:)]))

        [_soDelegate soTextViewWasTapped:self];
}

This delegate will tell the custom cell that the textView was tapped. My custom cell also have a delegate in order to trigger its actual selection.

Now you can tap on a link and it will open, you can tap on the textView and be notified and you still can select text and tap to deselect it.

Mecklenburg answered 26/4, 2014 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.