39

In an iOS 7 app, I have a UITextView with a link in it, but tapping the link doesn't fire. It only responds to an awkward "tap and hold". I want it to respond as soon as a user taps on it, like how a UIWebView link tap works. Here is my setup:

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."];
    [text addAttribute:NSLinkAttributeName value:@"myurl://tapped" range:NSMakeRange(6, 16)];

    self.textView.attributedText = text;
    self.textView.editable = NO;
    self.textView.delaysContentTouches = NO;
}

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
    if ([[URL scheme] isEqualToString:@"myurl"])
    {
        // Handle tap

        return NO;
    }

    return YES;
}

The Apple Documentation for the shouldInteractWithURL method states: "The text view calls this method if the user taps or long-presses the URL link". The long-press is working, but the tap doesn't seem to work.

Does anyone know how to get this to respond immediately?

Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
lehn0058
  • 19,977
  • 15
  • 69
  • 109

9 Answers9

41

If you still want to go with a native UITextView, you can add a tap recognizer to your textview and get the string attributes at the tap location. When you find a link, you can open it immediately.

I wrote a Gist that solves this for iOS 7/8. It's a lightweight extension of UITextView that also forwards -[UITextViewDelegate textView:shouldInteractWithURL:inRange:] and exposes the internal tap gesture recognizer.

https://gist.github.com/benjaminbojko/c92ac19fe4db3302bd28

Here's a quick example:The example below only works on iOS 8. See the gist above for iOS 7 + 8 support.

Add your tap recognizer:

// ...

UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedTextView:)];
[myTextView addGestureRecognizer:tapRecognizer];
myTextView.selectable = YES; // otherwise the gesture won't recognize

// ...

And add your callback:

- (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
    if (tapGesture.state != UIGestureRecognizerStateEnded) {
        return;
    }

    UITextView *textView = (UITextView *)tapGesture.view;
    CGPoint tapLocation = [tapGesture locationInView:textView];
    UITextPosition *textPosition = [textView closestPositionToPoint:tapLocation];
    NSDictionary *attributes = [textView textStylingAtPosition:textPosition inDirection:UITextStorageDirectionForward];

    NSURL *url = attributes[NSLinkAttributeName];

    if (url) {
        [[UIApplication sharedApplication] openURL:url];
    }
}

And swift version:

Tap recognizer:

let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedTextView:"))
myTextView.addGestureRecognizer(tapRecognizer)
myTextView.selectable = true

Callback:

func tappedTextView(tapGesture: UIGestureRecognizer) {

        let textView = tapGesture.view as! UITextView
        let tapLocation = tapGesture.locationInView(textView)
        let textPosition = textView.closestPositionToPoint(tapLocation)
        let attr: NSDictionary = textView.textStylingAtPosition(textPosition, inDirection: UITextStorageDirection.Forward)

        if let url: NSURL = attr[NSLinkAttributeName] as? NSURL {
            UIApplication.sharedApplication().openURL(url)
        }

        }
buxik
  • 2,583
  • 24
  • 31
Benjamin Bojko
  • 553
  • 1
  • 7
  • 14
  • 2
    I just tried this on iOS 7, and it looks like the textView can't find the link attribute at all. Have you had any luck with this on iOS 7? – Bob Vork Nov 24 '14 at 14:15
  • @BobVork Right, I remember having some cross-version issues. Ultimately, it became a little tricky to get the attributes with `textStylingAtPosition:inDirection:` on iOS 7. I assume that it had to do with the textPosition having a length of 0, and therefore the attributed string had no characters to actually host any link information. Just a thought. Ultimately I wrote this Gist that works for me across iOS 7 and 8: https://gist.github.com/benjaminbojko/c92ac19fe4db3302bd28 – Benjamin Bojko Nov 24 '14 at 16:57
  • Note that instead of "NSURL", it could be an "NSString" depending on how you setup the URL. – TheJeff Aug 26 '16 at 22:07
  • 1
    Swift 4: ```@objc func tappedTextView(tapGesture: UIGestureRecognizer) { let textView = tapGesture.view as! UITextView let tapLocation = tapGesture.location(in: textView) let textPosition = textView.closestPosition(to: tapLocation) let attr = textView.textStyling(at: textPosition!, in: .forward)! if let url: URL = attr[NSAttributedStringKey.link.rawValue] as? URL { UIApplication.shared.open(url, options: [:], completionHandler: nil) } }``` – Alan Scarpa Jan 09 '18 at 20:24
14

Is the UITextView selectable?. Try with:

self.textView.selectable = YES;

Edit:

I'm starting to think that maybe a long-press is the only way to fire it contrary to what apple says. Check this link, maybe it will help.

nnarayann
  • 1,449
  • 19
  • 24
  • It is selectable. The problem isn't that the link doesn't work. It's that the user has to do a delayed touch on it. – lehn0058 Mar 13 '14 at 13:24
  • Could be a bug now fixed in iOS 7.1? [link](http://forums.macrumors.com/showthread.php?t=1691855). Try disabling scrolling as workaround. – nnarayann Mar 13 '14 at 13:28
  • I am using 7.1 - just updated yesterday and verified this was how it works in both 7.0 and 7.1. – lehn0058 Mar 13 '14 at 13:30
  • 1
    Found possible workarounds: `[self.textView setText:nil]` before setting the URL. `self.textView.dataDetectorTypes = UIDataDetectorTypeLink;` `[self.textView.scrollEnabled:NO];` – nnarayann Mar 13 '14 at 13:43
  • Thanks - I tried these settings as well, but there was no change to the behavior. – lehn0058 Mar 13 '14 at 14:10
  • 2
    I'm starting to think that maybe a long-press is the only way to fire it contrary to what apple says. Check this [link](https://github.com/choefele/CCHLinkTextView), maybe it will help. – nnarayann Mar 13 '14 at 16:06
  • nnarayann, it will need a few modifications, but I this should give me what I want. If you add this suggestion into an answer, I will mark yours as correct. – lehn0058 Mar 13 '14 at 17:11
  • I am facing the same problem where the user has to long-press a link detected by `dataDetectorTypes = UIDataDetectorTypeLink`. Seems like CCHLinkTextView doesn't support quick tapping URL links unfortunately. – OpenUserX03 May 18 '14 at 04:29
7

As nnarayann mentioned, CCHLinkTextView avoids the problem of delayed tap recognition. This library implements its own gesture recognizer and is now available in version 1.0.

Claus
  • 331
  • 1
  • 3
  • 7
  • Nice library! It took only 2 minutes to include the pod and adjust existing code in my swift project. Works perfectly. Also, the attribute takes a value of any type instead of a NSURL. It is just perfect if you need to pass something else like an index. Thanks! – antoine Dec 16 '15 at 16:49
4

Swift 4 solution


  • Handles links using tap gesture recognizer
  • Whether isSelectable is true or false, it works

let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleLinkTap(_:)))
textView.addGestureRecognizer(tapRecognizer)

@objc func handleLinkTap(_ recognizer: UITapGestureRecognizer) {
    let tapLocation = recognizer.location(in: textView)
    guard
        let textPosition = textView.closestPosition(to: tapLocation),
        let url = textView.textStyling(at: textPosition, in: .forward)?[NSAttributedStringKey.link.rawValue],
        let urlString = (url as? String) ?? (url as? URL)?.absoluteString,
        urlString == "myurl"
    else { return }

    let url = URL(string: urlString)!
    // Do whatever you want with this URL, such as
    UIApplication.shared.openURL(url)
}
AamirR
  • 11,672
  • 4
  • 59
  • 73
  • Use this: `let urlString = textView.textStyling(at: textPosition, in: .forward)?[NSAttributedStringKey.link.rawValue] as? URL, urlString == "myurl" else { return }` Instead of: `let urlString = textView.textStyling(at: textPosition, in: .forward)?[NSAttributedStringKey.link.rawValue] as? String, urlString == "myurl" else { return }` – Rashesh Bosamiya Jan 08 '18 at 14:16
  • Are you sure? because (as I remember) I tried to cast the result as URL, I guess it failed, I will recheck to make sure (soon as possible) if it works – AamirR Jan 08 '18 at 18:10
  • 1
    Yes, I am sure. Check from your side as well – Rashesh Bosamiya Jan 08 '18 at 18:12
  • 1
    I have figure out, It depends on link attribute. I have given URL. It can also take String – Rashesh Bosamiya Jan 09 '18 at 05:22
  • Rechecked @RasheshBosamiya, value for `textStyling[NSAttributedStringKey.link.rawValue]` is NSString which fails on casting to URL, you can create `URL.init(string: urlString)` if necessary – AamirR Jan 09 '18 at 22:12
  • Yes, Have provided URL as link attribute. Thanks – Rashesh Bosamiya Jan 10 '18 at 10:33
  • Thanks for your comments, it saved me today *It depends on link attribute.* answer updated to support both cases – AamirR May 05 '18 at 18:22
  • 1
    Great! Nice to hear you after little long time. Thanks for be in touch. – Rashesh Bosamiya May 05 '18 at 18:28
2

Sometimes it's not working on the iOS Simulator or only work if you tap & hold.

You should test it on a real device.

And don't forget to set it selectable.

Laszlo
  • 2,803
  • 2
  • 28
  • 33
1

If there isn't any pressing reason to use a UITextView, such as there being other text being displayed, you could use a UILabel combined with a UITapGestureRecognizer to get the effect you're looking for.

Otherwise, you could go with an actual UIWebView instead.

dandan78
  • 13,328
  • 13
  • 64
  • 78
  • 1
    I am doing a lot of attributedString work in the UITextView, so I would prefer this over building an HTML string. Also, I need specific points of text to be selectable, not the entire text so UILabel is out too. – lehn0058 Mar 13 '14 at 13:26
  • 1
    Yeah, but then there is no touch highlight. – mxcl Oct 29 '17 at 19:18
1

Have you tried setting textview.delaysContentTouches = NO; ? Maybe that could help.

dezinezync
  • 2,730
  • 21
  • 15
  • I have - that was my first thought and is included in the code above. I know that UITextView inherits from UIScrollView, so I thought it was related to that somehow. – lehn0058 Mar 13 '14 at 13:23
  • that was my first instinct as well. I'd then recommend you go ahead with working your way through `touchesBegan` & it's brothers and `attributesAtIndex` and check for the `NSLinkAttribute` if it exists. – dezinezync Mar 13 '14 at 13:29
  • How would you go about doing this? This sounds like re-implementing the link detection... – lehn0058 Mar 13 '14 at 14:10
  • Also, where is the attributesAtIndex method? – lehn0058 Mar 13 '14 at 15:07
  • Yes, it does seem so. The attributesAtIndex is available on the NSAttributedString of that UITextView. I can post some code later tomorrow – dezinezync Mar 13 '14 at 17:12
0

If you don't need editing, you can use this:

textView.isEditable = true
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool
{
    return false
}
El Horrible
  • 161
  • 9
0

Use this method

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        return true
    }
zaid afzal
  • 119
  • 1
  • 5