11

I have a textView that is layered on a tableView cell. I need the user to click on the tableView row to open a viewController but if the textView has a link it must be clickable and that's it, no copy, select etc.

If I allow user interaction like this:

textView.userInteractionEnabled=NO;
textView.dataDetectorTypes =UIDataDetectorTypeLink;

the didSelectRowAtIndexPath is not called and I understand why but how to achieve this?

EDIT:

What I'm looking for is the ability to specifically identify link and allow user to click on the link to open a browser and disable interaction for the rest of the textView. Also the entire row should be clickable as said above.

Attached is the image of my tableView cell row which shows the link is detected and interaction is also possible but this disables the didSelectRowAtIndexPath inside the textView region.

enter image description here

Atif Imran
  • 1,899
  • 2
  • 18
  • 33
  • Have you tried this delegate - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange. I haven't tried this yet. – HRM Jan 09 '14 at 08:34
  • No, but i think that should deal with the text contained in the textView. What I'm looking for is the ability to specifically identify link and allow user to interact for that part and not for the whole textView. – Atif Imran Jan 09 '14 at 08:36
  • If you have only one link always, then you can just do your functionality in didSelectRowAtIndexPath itself by checking whether the selected row contains link. But if you have multiple links, then you may try with uitapgesturerecognizer on textview (I guess.not sure) – HRM Jan 09 '14 at 08:42
  • This may help- http://stackoverflow.com/questions/5997708/uitextview-inside-uitableview – HRM Jan 09 '14 at 08:45
  • And what are you using for textview?? custom class? – Toseef Khilji Jan 09 '14 at 10:18

4 Answers4

8

If you are trying to add a UITextView with links to a cell, and want didSelectRow to be called when the user taps on anything but the link, then you should use hitTest:withEvent:

Swift 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // location of the tap
    var location = point
    location.x -= self.textContainerInset.left
    location.y -= self.textContainerInset.top

    // find the character that's been tapped
    let characterIndex = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    if characterIndex < self.textStorage.length {
        // if the character is a link, handle the tap as UITextView normally would
        if (self.textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil) {
            return self
        }
    }

    // otherwise return nil so the tap goes on to the next receiver
    return nil
}

I wrote an article about this with a bit more details.

Saoud Rizwan
  • 629
  • 10
  • 20
  • UITextView 0x7f9beb166400 is switching to TextKit 1 compatibility mode because its layoutManager was accessed. Getting this in iOS 16, Please guide what to be done. – the monk Sep 15 '22 at 06:52
3

One possible (and complex) solution would be using UIViews with UITapGestureRecogniser inside your UITextView.

  • Firstly, you will need to find the NSRange of your link.

  • Convert NSRange to UITextRange

  • Use code similar to the following to add a UITapGestureRecogniser right on top of the link-text in your UITextView.

    UITextPosition *pos = textView.endOfDocument;// textView ~ UITextView
    
    for (int i = 0; i < words*2 - 1; i++){// *2 since UITextGranularityWord considers a whitespace to be a word
    
        UITextPosition *pos2 = [textView.tokenizer positionFromPosition:pos toBoundary:UITextGranularityWord inDirection:UITextLayoutDirectionLeft];
        UITextRange *range = [textView textRangeFromPosition:pos toPosition:pos2];
        CGRect resultFrame = [textView firstRectForRange:(UITextRange *)range ];
    
        UIView* tapViewOnText = [[UIView alloc] initWithFrame:resultFrame];
        [tapViewOnText addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(targetRoutine)]];
        tapViewOnText.tag = 125;
        [textView addSubview:tapViewOnText];
        [tapViewOnText release];
    
        pos = pos2;
    

    }

What I have done in this code is, got the UITextRange of relevant text, get it's firstRectForRange and added a transparent tap-able UIView right on top of it.

You would have to get the range of your link using some regEx, convert it to UITextRange, and add tap-able UIViews over them. In case, there might be more than one link in a single textView you might add a tag to each view corresponding to their 'link', and open that link in the target method by checking it's tag.

NOTE: If your UITextViews are universally un-editable, you might want to try TTTAttributedLabel instead. That is what I do in my UITableViewCells

n00bProgrammer
  • 4,261
  • 3
  • 32
  • 60
  • Thanks, it is indeed a bit complex and I'm hoping to get any easier solution before i dive into this at last – Atif Imran Jan 09 '14 at 10:50
  • Have you given `TTTAttributedLabel` a look ? It's pretty nifty. Let's you even call your own methods by adding 'links' to your text. – n00bProgrammer Jan 09 '14 at 11:29
  • In the meantime, i'll try to think of something simpler, and less-taxing. – n00bProgrammer Jan 09 '14 at 11:30
  • if tap able view can be added then why not uibutton? – amar Mar 15 '14 at 08:21
  • You get the frame by this method. Once you have that, you can add any UI element as you like. `UIView`s are preferred since they are economical memory wise, compared to `UIButton`s. Ultimately, the choice is yours. :) – n00bProgrammer Mar 15 '14 at 08:49
0

Maybe it will be ok for your purposes:

textView.editable=NO;
textView.userInteractionEnabled=YES;
textView.dataDetectorTypes =UIDataDetectorTypeLink;

But in fact UITextView uses UIWebView parts internally, and this way may be slowly in table cells. enter image description here

I can advice to use CoreText or NSAttributedString's with touches. For example you may read this SO post.

Community
  • 1
  • 1
Alex Nazarov
  • 1,178
  • 8
  • 16
0

Swift 5, iOS14 Adjusting @Saoud Rizwan Answer because hitTest did not work well for me (it always called the same position multiple times). However here's my solution using UITapGestureRecognizer, where a tap on a link inside a UITextView will be detected.

First configure your Tap event to the UITextView

contentTextView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(textViewTapped(_:))))

The CellTapped implementation:

@objc func cellTapped(_ sender: UITapGestureRecognizer) {
    var location = sender.location(in: contentTextView)
    location.x -= contentTextView.textContainerInset.left
    location.y -= contentTextView.textContainerInset.top

    let characterIndex = contentTextView.layoutManager.characterIndex(for: location, in: contentTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    if characterIndex < contentTextView.textStorage.length {
        if let url = contentTextView.textStorage.attribute(.link, at: characterIndex, effectiveRange: nil) as? URL {
            // The user tapped on a link!
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url)
                return
            }
        }
    }

    // here: The user tapped somewhere in the textView but not on a link!
}
Lirik
  • 3,167
  • 1
  • 30
  • 31
  • 1
    contentTextView.layoutManager.characterIndex(...) this function is main in this use case but in iOS 16 UITextView is using TextKit 2 and use of layoutManager is cautioned. How to work with TextKit 2 – the monk Sep 15 '22 at 13:25