Create tap-able “links” in the NSAttributedString of a UILabel?

I have been searching this for hours but I’ve failed. I probably don’t even know what I should be looking for.

Many applications have text and in this text are web hyperlinks in rounded rect. When I click them UIWebView opens. What puzzles me is that they often have custom links, for example if words starts with # it is also clickable and the application responds by opening another view. How can I do that? Is it possible with UILabel or do I need UITextView or something else?

  • Handling applicationDidBecomeActive - “How can a view controller respond to the app becoming Active?”
  • Shared UITableViewDelegate
  • Take screenshot from camera view
  • Adding a view (image view) in between two UITableViewCells
  • Embedding YouTube videos on
  • KVO broken in iOS 9.3
  • 23 Solutions Collect From Internet About “Create tap-able “links” in the NSAttributedString of a UILabel?”

    In general, if we want to have a clickable link in text displayed by UILabel, we would need to resolve two independent tasks:

    1. Changing the appearance of a portion of the text to look like a link
    2. Detecting and handling touches on the link (opening an URL is a particular case)

    The first one is easy. Starting from iOS 6 UILabel supports display of attributed strings. All you need to do is to create and configure an instance of NSMutableAttributedString:

    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"String with a link" attributes:nil];
    NSRange linkRange = NSMakeRange(14, 4); // for the word "link" in the string above
    
    NSDictionary *linkAttributes = @{ NSForegroundColorAttributeName : [UIColor colorWithRed:0.05 green:0.4 blue:0.65 alpha:1.0],
                                      NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) };
    [attributedString setAttributes:linkAttributes range:linkRange];
    
    // Assign attributedText to UILabel
    label.attributedText = attributedString;
    

    That’s it! The code above makes UILabel to display String with a link

    Now we should detect touches on this link. The idea is to catch all taps within UILabel and figure out whether the location of the tap was close enough to the link. To catch touches we can add tap gesture recognizer to the label. Make sure to enable userInteraction for the label, it’s turned off by default:

    label.userInteractionEnabled = YES;
    [label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapOnLabel:)]]; 
    

    Now the most sophisticated stuff: finding out whether the tap was on where the link is displayed and not on any other portion of the label. If we had single-lined UILabel, this task could be solved relatively easy by hardcoding the area bounds where the link is displayed, but let’s solve this problem more elegantly and for general case – multiline UILabel without preliminary knowledge about the link layout.

    One of the approaches is to use capabilities of Text Kit API introduced in iOS 7:

    // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
    
    // Configure layoutManager and textStorage
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];
    
    // Configure textContainer
    textContainer.lineFragmentPadding = 0.0;
    textContainer.lineBreakMode = label.lineBreakMode;
    textContainer.maximumNumberOfLines = label.numberOfLines;
    

    Save created and configured instances of NSLayoutManager, NSTextContainer and NSTextStorage in properties in your class (most likely UIViewController’s descendant) – we’ll need them in other methods.

    Now, each time the label changes its frame, update textContainer’s size:

    - (void)viewDidLayoutSubviews
    {
        [super viewDidLayoutSubviews];
        self.textContainer.size = self.label.bounds.size;
    }
    

    And finally, detect whether the tap was exactly on the link:

    - (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture
    {
        CGPoint locationOfTouchInLabel = [tapGesture locationInView:tapGesture.view];
        CGSize labelSize = tapGesture.view.bounds.size;
        CGRect textBoundingBox = [self.layoutManager usedRectForTextContainer:self.textContainer];
        CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                  (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
        CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                             locationOfTouchInLabel.y - textContainerOffset.y);
        NSInteger indexOfCharacter = [self.layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                                inTextContainer:self.textContainer
                                       fractionOfDistanceBetweenInsertionPoints:nil];
        NSRange linkRange = NSMakeRange(14, 4); // it's better to save the range somewhere when it was originally used for marking link in attributed string
        if (NSLocationInRange(indexOfCharacter, linkRange)) {
            // Open an URL, or handle the tap on the link in any other way
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://stackoverflow.com/"]];
        }
    }
    

    FancyLabel is exactly what I needed 🙂

    The UIButtonTypeCustom is a clickable label if you don’t set any images for it.

    (My answer builds on @NAlexN’s excellent answer. I won’t duplicate his detailed explanation of each step here.)

    I found it most convenient and straightforward to add support for tap-able UILabel text as a category to UITapGestureRecognizer. (You don’t have to use UITextView’s data detectors, as some answers suggest.)

    Add the following method to your UITapGestureRecognizer category:

    /**
     Returns YES if the tap gesture was within the specified range of the attributed text of the label.
     */
    - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange {
        NSParameterAssert(label != nil);
    
        CGSize labelSize = label.bounds.size;
        // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
    
        // configure layoutManager and textStorage
        [layoutManager addTextContainer:textContainer];
        [textStorage addLayoutManager:layoutManager];
    
        // configure textContainer for the label
        textContainer.lineFragmentPadding = 0.0;
        textContainer.lineBreakMode = label.lineBreakMode;
        textContainer.maximumNumberOfLines = label.numberOfLines;
        textContainer.size = labelSize;
    
        // find the tapped character location and compare it to the specified range
        CGPoint locationOfTouchInLabel = [self locationInView:label];
        CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
        CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                  (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
        CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                             locationOfTouchInLabel.y - textContainerOffset.y);
        NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                                inTextContainer:textContainer
                                       fractionOfDistanceBetweenInsertionPoints:nil];
        if (NSLocationInRange(indexOfCharacter, targetRange)) {
            return YES;
        } else {
            return NO;
        }
    }
    

    Example Code

    // (in your view controller)    
    // create your label, gesture recognizer, attributed text, and get the range of the "link" in your label
    myLabel.userInteractionEnabled = YES;
    [myLabel addGestureRecognizer:
       [[UITapGestureRecognizer alloc] initWithTarget:self 
                                               action:@selector(handleTapOnLabel:)]]; 
    
    // create your attributed text and keep an ivar of your "link" text range
    NSAttributedString *plainText;
    NSAttributedString *linkText;
    plainText = [[NSMutableAttributedString alloc] initWithString:@"Add label links with UITapGestureRecognizer"
                                                       attributes:nil];
    linkText = [[NSMutableAttributedString alloc] initWithString:@" Learn more..."
                                                      attributes:@{
                                                          NSForegroundColorAttributeName:[UIColor blueColor]
                                                      }];
    NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] init];
    [attrText appendAttributedString:plainText];
    [attrText appendAttributedString:linkText];
    
    // ivar -- keep track of the target range so you can compare in the callback
    targetRange = NSMakeRange(plainText.length, linkText.length);
    

    Gesture Callback

    // handle the gesture recognizer callback and call the category method
    - (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture {
        BOOL didTapLink = [tapGesture didTapAttributedTextInLabel:myLabel
                                                inRange:targetRange];
        NSLog(@"didTapLink: %d", didTapLink);
    
    }
    

    I am extending @NAlexN original detailed solution, with @zekel excellent extension of UITapGestureRecognizer, and providing in Swift.

    Extending UITapGestureRecognizer

    extension UITapGestureRecognizer {
    
        func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: label.attributedText!)
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
    
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = self.locationInView(label)
            let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
            let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
            let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                locationOfTouchInLabel.y - textContainerOffset.y);
            let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            return NSLocationInRange(indexOfCharacter, targetRange)
        }
    
    }
    

    Usage

    Setup UIGestureRecognizer to send actions to tapLabel:, and you can detect if the target ranges is being tapped on in myLabel.

    @IBAction func tapLabel(gesture: UITapGestureRecognizer) {
        if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange1) {
            print("Tapped targetRange1")
        } else if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange2) {
            print("Tapped targetRange2")
        } else {
            print("Tapped none")
        }
    }
    

    UITextView supports data-detectors in OS3.0, whereas UILabel doesn’t.

    If you enable the data-detectors on the UITextView and your text contains URLs, phone numbers, etc. they will appear as links.

    Old question but if anyone can use a UITextView instead of a UILabel, then it is easy. Standard URLs, phone numbers etc will be automatically detected (and be clickable).

    However, if you need custom detection, that is, if you want to be able to call any custom method after a user clicks on a particular word, you need to use NSAttributedStrings with an NSLinkAttributeName attribute that will point to a custom URL scheme(as opposed to having the http url scheme by default). Ray Wenderlich has it covered here

    Quoting the code from the aforementioned link:

    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"This is an example by @marcelofabri_"];
    [attributedString addAttribute:NSLinkAttributeName
                         value:@"username://marcelofabri_"
                         range:[[attributedString string] rangeOfString:@"@marcelofabri_"]];
    
    NSDictionary *linkAttributes = @{NSForegroundColorAttributeName: [UIColor greenColor],
                                 NSUnderlineColorAttributeName: [UIColor lightGrayColor],
                                 NSUnderlineStyleAttributeName: @(NSUnderlinePatternSolid)};
    
    // assume that textView is a UITextView previously created (either by code or Interface Builder)
    textView.linkTextAttributes = linkAttributes; // customizes the appearance of links
    textView.attributedText = attributedString;
    textView.delegate = self;
    

    To detect those link clicks, implement this:

    - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
        if ([[URL scheme] isEqualToString:@"username"]) {
            NSString *username = [URL host]; 
            // do something with this username
            // ...
            return NO;
        }
        return YES; // let the system open this URL
    }
    

    PS: Make sure your UITextView is selectable.

    Here is example code to hyperlink UILabel:
    Source:http://sickprogrammersarea.blogspot.in/2014/03/adding-links-to-uilabel.html

    #import "ViewController.h"
    #import "TTTAttributedLabel.h"
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    {
    UITextField *loc;
    TTTAttributedLabel *data;
    }
    
    - (void)viewDidLoad
    {
    [super viewDidLoad];
    UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(5, 20, 80, 25) ];
    [lbl setText:@"Text:"];
    [lbl setFont:[UIFont fontWithName:@"Verdana" size:16]];
    [lbl setTextColor:[UIColor grayColor]];
    loc=[[UITextField alloc] initWithFrame:CGRectMake(4, 20, 300, 30)];
    //loc.backgroundColor = [UIColor grayColor];
    loc.borderStyle=UITextBorderStyleRoundedRect;
    loc.clearButtonMode=UITextFieldViewModeWhileEditing;
    //[loc setText:@"Enter Location"];
    loc.clearsOnInsertion = YES;
    loc.leftView=lbl;
    loc.leftViewMode=UITextFieldViewModeAlways;
    [loc setDelegate:self];
    [self.view addSubview:loc];
    [loc setRightViewMode:UITextFieldViewModeAlways];
    CGRect frameimg = CGRectMake(110, 70, 70,30);
    UIButton *srchButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    srchButton.frame=frameimg;
    [srchButton setTitle:@"Go" forState:UIControlStateNormal];
    [srchButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    srchButton.backgroundColor=[UIColor clearColor];
    [srchButton addTarget:self action:@selector(go:) forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:srchButton];
    data = [[TTTAttributedLabel alloc] initWithFrame:CGRectMake(5, 120,self.view.frame.size.width,200) ];
    [data setFont:[UIFont fontWithName:@"Verdana" size:16]];
    [data setTextColor:[UIColor blackColor]];
    data.numberOfLines=0;
     data.delegate = self;
    data.enabledTextCheckingTypes=NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber;
    [self.view addSubview:data];
    }
    - (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url
    {
    NSString *val=[[NSString alloc]initWithFormat:@"%@",url];
    if ([[url scheme] hasPrefix:@"mailto"]) {
              NSLog(@" mail URL Selected : %@",url);
        MFMailComposeViewController *comp=[[MFMailComposeViewController alloc]init];
        [comp setMailComposeDelegate:self];
        if([MFMailComposeViewController canSendMail])
        {
            NSString *recp=[[val substringToIndex:[val length]] substringFromIndex:7];
            NSLog(@"Recept : %@",recp);
            [comp setToRecipients:[NSArray arrayWithObjects:recp, nil]];
            [comp setSubject:@"From my app"];
            [comp setMessageBody:@"Hello bro" isHTML:NO];
            [comp setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
            [self presentViewController:comp animated:YES completion:nil];
        }
    
    }
    else{
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:val]];
    }
    }
    -(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error{
    if(error)
    {
        UIAlertView *alrt=[[UIAlertView alloc]initWithTitle:@"Erorr" message:@"Some error occureed" delegate:nil cancelButtonTitle:@"" otherButtonTitles:nil, nil];
        [alrt show];
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    else{
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    }
    
    - (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithPhoneNumber:(NSString *)phoneNumber
    {
    NSLog(@"Phone Number Selected : %@",phoneNumber);
    UIDevice *device = [UIDevice currentDevice];
    if ([[device model] isEqualToString:@"iPhone"] ) {
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"tel:%@",phoneNumber]]];
    } else {
        UIAlertView *Notpermitted=[[UIAlertView alloc] initWithTitle:@"Alert" message:@"Your device doesn't support this feature." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [Notpermitted show];
    }
    }
    -(void)go:(id)sender
    {
    [data setText:loc.text];
    }
    
    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    NSLog(@"Reached");
    [loc resignFirstResponder];
    }
    

    I created UILabel subclass named ResponsiveLabel which is based on textkit API introduced in iOS 7. It uses the same approach suggested by NAlexN. It provides flexibility to specify a pattern to search in the text. One can specify styles to be applied to those patterns as well as action to be performed on tapping the patterns.

    //Detects email in text
    
     NSString *emailRegexString = @"[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}";
     NSError *error;
     NSRegularExpression *regex = [[NSRegularExpression alloc]initWithPattern:emailRegexString options:0 error:&error];
     PatternDescriptor *descriptor = [[PatternDescriptor alloc]initWithRegex:regex withSearchType:PatternSearchTypeAll withPatternAttributes:@{NSForegroundColorAttributeName:[UIColor redColor]}];
     [self.customLabel enablePatternDetection:descriptor];
    

    If you want to make a string clickable, you can do this way. This code applies attributes to each occurrence of the string “text”.

    PatternTapResponder tapResponder = ^(NSString *string) {
        NSLog(@"tapped = %@",string);
    };
    
    [self.customLabel enableStringDetection:@"text" withAttributes:@{NSForegroundColorAttributeName:[UIColor redColor],
                                                                     RLTapResponderAttributeName: tapResponder}];
    

    As I mentioned in this post,
    here is a light-weighted library I created specially for links in UILabel FRHyperLabel.

    To achieve an effect like this:

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis blandit eros, sit amet vehicula justo. Nam at urna neque. Maecenas ac sem eu sem porta dictum nec vel tellus.

    use code:

    //Step 1: Define a normal attributed string for non-link texts
    NSString *string = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis blandit eros, sit amet vehicula justo. Nam at urna neque. Maecenas ac sem eu sem porta dictum nec vel tellus.";
    NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]};
    
    label.attributedText = [[NSAttributedString alloc]initWithString:string attributes:attributes];
    
    
    //Step 2: Define a selection handler block
    void(^handler)(FRHyperLabel *label, NSString *substring) = ^(FRHyperLabel *label, NSString *substring){
        NSLog(@"Selected: %@", substring);
    };
    
    
    //Step 3: Add link substrings
    [label setLinksForSubstrings:@[@"Lorem", @"Pellentesque", @"blandit", @"Maecenas"] withLinkHandler:handler];
    

    Worked in Swift 3, pasting the entire code here

        //****Make sure the textview 'Selectable' = checked, and 'Editable = Unchecked'
    
    import UIKit
    
    class ViewController: UIViewController, UITextViewDelegate {
    
        @IBOutlet var theNewTextView: UITextView!
        override func viewDidLoad() {
            super.viewDidLoad()
    
            //****textview = Selectable = checked, and Editable = Unchecked
    
            theNewTextView.delegate = self
    
            let theString = NSMutableAttributedString(string: "Agree to Terms")
            let theRange = theString.mutableString.range(of: "Terms")
    
            theString.addAttribute(NSLinkAttributeName, value: "ContactUs://", range: theRange)
    
            let theAttribute = [NSForegroundColorAttributeName: UIColor.blue, NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue] as [String : Any]
    
            theNewTextView.linkTextAttributes = theAttribute
    
         theNewTextView.attributedText = theString             
    
    theString.setAttributes(theAttribute, range: theRange)
    
        }
    
        func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    
            if (URL.scheme?.hasPrefix("ContactUs://"))! {
    
                return false //interaction not allowed
            }
    
            //*** Set storyboard id same as VC name
            self.navigationController!.pushViewController((self.storyboard?.instantiateViewController(withIdentifier: "TheLastViewController"))! as UIViewController, animated: true)
    
            return true
        }
    
    }
    

    For fully custom links, you’ll need to use a UIWebView – you can intercept the calls out, so that you can go to some other part of your app instead when a link is pressed.

    Here is a swift version of NAlexN’s answer.

    class TapabbleLabel: UILabel {
    
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize.zero)
    var textStorage = NSTextStorage() {
        didSet {
            textStorage.addLayoutManager(layoutManager)
        }
    }
    
    var onCharacterTapped: ((label: UILabel, characterIndex: Int) -> Void)?
    
    let tapGesture = UITapGestureRecognizer()
    
    override var attributedText: NSAttributedString? {
        didSet {
            if let attributedText = attributedText {
                textStorage = NSTextStorage(attributedString: attributedText)
            } else {
                textStorage = NSTextStorage()
            }
        }
    }
    
    override var lineBreakMode: NSLineBreakMode {
        didSet {
            textContainer.lineBreakMode = lineBreakMode
        }
    }
    
    override var numberOfLines: Int {
        didSet {
            textContainer.maximumNumberOfLines = numberOfLines
        }
    }
    
    /**
     Creates a new view with the passed coder.
    
     :param: aDecoder The a decoder
    
     :returns: the created new view.
     */
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }
    
    /**
     Creates a new view with the passed frame.
    
     :param: frame The frame
    
     :returns: the created new view.
     */
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }
    
    /**
     Sets up the view.
     */
    func setUp() {
        userInteractionEnabled = true
        layoutManager.addTextContainer(textContainer)
        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = lineBreakMode
        textContainer.maximumNumberOfLines = numberOfLines
        tapGesture.addTarget(self, action: #selector(TapabbleLabel.labelTapped(_:)))
        addGestureRecognizer(tapGesture)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        textContainer.size = bounds.size
    }
    
    func labelTapped(gesture: UITapGestureRecognizer) {
        guard gesture.state == .Ended else {
            return
        }
    
        let locationOfTouch = gesture.locationInView(gesture.view)
        let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
        let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2 - textBoundingBox.minX,
                                          y: (bounds.height - textBoundingBox.height) / 2 - textBoundingBox.minY)        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouch.x - textContainerOffset.x,
                                                     y: locationOfTouch.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
                                                                    inTextContainer: textContainer,
                                                                    fractionOfDistanceBetweenInsertionPoints: nil)
    
        onCharacterTapped?(label: self, characterIndex: indexOfCharacter)
    }
    

    }

    You can then create an instance of that class inside your viewDidLoad method like this:

    let label = TapabbleLabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[view]-|",
                                                   options: [], metrics: nil, views: ["view" : label]))
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[view]-|",
                                                   options: [], metrics: nil, views: ["view" : label]))
    
    let attributedString = NSMutableAttributedString(string: "String with a link", attributes: nil)
    let linkRange = NSMakeRange(14, 4); // for the word "link" in the string above
    
    let linkAttributes: [String : AnyObject] = [
        NSForegroundColorAttributeName : UIColor.blueColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue,
        NSLinkAttributeName: "http://www.apple.com"]
    attributedString.setAttributes(linkAttributes, range:linkRange)
    
    label.attributedText = attributedString
    
    label.onCharacterTapped = { label, characterIndex in
        if let attribute = label.attributedText?.attribute(NSLinkAttributeName, atIndex: characterIndex, effectiveRange: nil) as? String,
            let url = NSURL(string: attribute) {
            UIApplication.sharedApplication().openURL(url)
        }
    }
    

    It’s better to have a custom attribute to use when a character is tapped. Now, it’s the NSLinkAttributeName, but could be anything and you can use that value to do other things other than opening a url, you can do any custom action.

    I had a hard time dealing with this… UILabel with links on it on attributed text… it is just a headache so I ended up using ZSWTappableLabel.

    I’d strongly recommend using a library that automatically detects URLs in text and converts them to links.
    Try:

    • TTTAttributedLabel (pod)
    • ZSWTappableLabel (pod).

    Both are under MIT license.

    I’m extending @samwize’s answer to handle multi-line UILabel and give an example on using for a UIButton

    extension UITapGestureRecognizer {
    
        func didTapAttributedTextInButton(button: UIButton, inRange targetRange: NSRange) -> Bool {
            guard let label = button.titleLabel else { return false }
            return didTapAttributedTextInLabel(label, inRange: targetRange)
        }
    
        func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: label.attributedText!)
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
    
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = self.locationInView(label)
            let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
            let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                  (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
            let locationOfTouchInTextContainer = CGPointMake((locationOfTouchInLabel.x - textContainerOffset.x),
                                                             0 );
            // Adjust for multiple lines of text
            let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
            let rightMostFirstLinePoint = CGPointMake(labelSize.width, 0)
            let charsPerLine = layoutManager.characterIndexForPoint(rightMostFirstLinePoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
    
            return NSLocationInRange(adjustedRange, targetRange)
        }
    
    }
    

    Like there is reported in earlier awnser the UITextView is able to handle touches on links. This can easily be extended by making other parts of the text work as links. The AttributedTextView library is a UITextView subclass that makes it very easy to handle these. For more info see: https://github.com/evermeer/AttributedTextView

    You can make any part of the text interact like this (where textView1 is a UITextView IBoutlet):

    textView1.attributer =
        "1. ".red
        .append("This is the first test. ").green
        .append("Click on ").black
        .append("evict.nl").makeInteract { _ in
            UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in })
        }.underline
        .append(" for testing links. ").black
        .append("Next test").underline.makeInteract { _ in
            print("NEXT")
        }
        .all.font(UIFont(name: "SourceSansPro-Regular", size: 16))
        .setLinkColor(UIColor.purple) 
    

    And for handling hashtags and mentions you can use code like this:

    textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library"
        .matchHashtags.underline
        .matchMentions
        .makeInteract { link in
            UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in })
        }
    

    Create the class with the following .h and .m files. In the .m file there is the following function

     - (void)linkAtPoint:(CGPoint)location
    

    Inside this function we will check the ranges of substrings for which we need to give actions. Use your own logic to put your ranges.

    And following is the usage of the subclass

    TaggedLabel *label = [[TaggedLabel alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [self.view addSubview:label];
    label.numberOfLines = 0;
    NSMutableAttributedString *attributtedString = [[NSMutableAttributedString alloc] initWithString : @"My name is @jjpp" attributes : @{ NSFontAttributeName : [UIFont systemFontOfSize:10],}];                                                                                                                                                                              
    //Do not forget to add the font attribute.. else it wont work.. it is very important
    [attributtedString addAttribute:NSForegroundColorAttributeName
                            value:[UIColor redColor]
                            range:NSMakeRange(11, 5)];//you can give this range inside the .m function mentioned above
    

    following is the .h file

    #import <UIKit/UIKit.h>
    
    @interface TaggedLabel : UILabel<NSLayoutManagerDelegate>
    
    @property(nonatomic, strong)NSLayoutManager *layoutManager;
    @property(nonatomic, strong)NSTextContainer *textContainer;
    @property(nonatomic, strong)NSTextStorage *textStorage;
    @property(nonatomic, strong)NSArray *tagsArray;
    @property(readwrite, copy) tagTapped nameTagTapped;
    
    @end   
    

    following is the .m file

    #import "TaggedLabel.h"
    @implementation TaggedLabel
    
    - (id)initWithFrame:(CGRect)frame
    {
     self = [super initWithFrame:frame];
     if (self)
     {
      self.userInteractionEnabled = YES;
     }
    return self;
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
     self = [super initWithCoder:aDecoder];
    if (self)
    {
     self.userInteractionEnabled = YES;
    }
    return self;
    }
    
    - (void)setupTextSystem
    {
     _layoutManager = [[NSLayoutManager alloc] init];
     _textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
     _textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
     // Configure layoutManager and textStorage
     [_layoutManager addTextContainer:_textContainer];
     [_textStorage addLayoutManager:_layoutManager];
     // Configure textContainer
     _textContainer.lineFragmentPadding = 0.0;
     _textContainer.lineBreakMode = NSLineBreakByWordWrapping;
     _textContainer.maximumNumberOfLines = 0;
     self.userInteractionEnabled = YES;
     self.textContainer.size = self.bounds.size;
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
     if (!_layoutManager)
     {
      [self setupTextSystem];
     }
     // Get the info for the touched link if there is one
     CGPoint touchLocation = [[touches anyObject] locationInView:self];
     [self linkAtPoint:touchLocation];
    }
    
    - (void)linkAtPoint:(CGPoint)location
    {
     // Do nothing if we have no text
     if (_textStorage.string.length == 0)
     {
      return;
     }
     // Work out the offset of the text in the view
     CGPoint textOffset = [self calcGlyphsPositionInView];
     // Get the touch location and use text offset to convert to text cotainer coords
     location.x -= textOffset.x;
     location.y -= textOffset.y;
     NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
     // If the touch is in white space after the last glyph on the line we don't
     // count it as a hit on the text
     NSRange lineRange;
     CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
     if (CGRectContainsPoint(lineRect, location) == NO)
     {
      return;
     }
     // Find the word that was touched and call the detection block
        NSRange range = NSMakeRange(11, 5);//for this example i'm hardcoding the range here. In a real scenario it should be iterated through an array for checking all the ranges
        if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
        {
         NSLog(@"range-->>%@",self.tagsArray[i][@"range"]);
        }
    }
    
    - (CGPoint)calcGlyphsPositionInView
    {
     CGPoint textOffset = CGPointZero;
     CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
     textBounds.size.width = ceil(textBounds.size.width);
     textBounds.size.height = ceil(textBounds.size.height);
    
     if (textBounds.size.height < self.bounds.size.height)
     {
      CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
      textOffset.y = paddingHeight;
     }
    
     if (textBounds.size.width < self.bounds.size.width)
     {
      CGFloat paddingHeight = (self.bounds.size.width - textBounds.size.width) / 2.0;
      textOffset.x = paddingHeight;
     }
     return textOffset;
     }
    
    @end
    

    based on Charles Gamble answer, this what I used (I removed some lines that confused me and gave me wrong indexed) :

    - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange TapGesture:(UIGestureRecognizer*) gesture{
        NSParameterAssert(label != nil);
    
        // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
    
        // configure layoutManager and textStorage
        [textStorage addLayoutManager:layoutManager];
    
        // configure textContainer for the label
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height)];
    
        textContainer.lineFragmentPadding = 0.0;
        textContainer.lineBreakMode = label.lineBreakMode;
        textContainer.maximumNumberOfLines = label.numberOfLines;
    
        // find the tapped character location and compare it to the specified range
        CGPoint locationOfTouchInLabel = [gesture locationInView:label];
        [layoutManager addTextContainer:textContainer]; //(move here, not sure it that matter that calling this line after textContainer is set
    
        NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInLabel
                                                               inTextContainer:textContainer
                                      fractionOfDistanceBetweenInsertionPoints:nil];
        if (NSLocationInRange(indexOfCharacter, targetRange)) {
            return YES;
        } else {
            return NO;
        }
    }
    

    Here’s a drop-in Objective-C category that enables clickable links in existing UILabel.attributedText strings, exploiting the existing NSLinkAttributeName attribute.

    @interface UILabel (GSBClickableLinks) <UIGestureRecognizerDelegate>
    @property BOOL enableLinks;
    @end
    
    #import <objc/runtime.h>
    static const void *INDEX;
    static const void *TAP;
    
    @implementation UILabel (GSBClickableLinks)
    
    - (void)setEnableLinks:(BOOL)enableLinks
    {
        UITapGestureRecognizer *tap = objc_getAssociatedObject(self, &TAP); // retreive tap
        if (enableLinks && !tap) { // add a gestureRegonzier to the UILabel to detect taps
            tap = [UITapGestureRecognizer.alloc initWithTarget:self action:@selector(openLink)];
            tap.delegate = self;
            [self addGestureRecognizer:tap];
            objc_setAssociatedObject(self, &TAP, tap, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save tap
        }
        self.userInteractionEnabled = enableLinks; // note - when false UILAbel wont receive taps, hence disable links
    }
    
    - (BOOL)enableLinks
    {
        return (BOOL)objc_getAssociatedObject(self, &TAP); // ie tap != nil
    }
    
    // First check whether user tapped on a link within the attributedText of the label.
    // If so, then the our label's gestureRecogizer will subsequently fire, and open the corresponding NSLinkAttributeName.
    // If not, then the tap will get passed along, eg to the enclosing UITableViewCell...
    // Note: save which character in the attributedText was clicked so that we dont have to redo everything again in openLink.
    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
    {
        if (gestureRecognizer != objc_getAssociatedObject(self, &TAP)) return YES; // dont block other gestures (eg swipe)
    
        // Re-layout the attributedText to find out what was tapped
        NSTextContainer *textContainer = [NSTextContainer.alloc initWithSize:self.frame.size];
        textContainer.lineFragmentPadding = 0;
        textContainer.maximumNumberOfLines = self.numberOfLines;
        textContainer.lineBreakMode = self.lineBreakMode;
        NSLayoutManager *layoutManager = NSLayoutManager.new;
        [layoutManager addTextContainer:textContainer];
        NSTextStorage *textStorage = [NSTextStorage.alloc initWithAttributedString:self.attributedText];
        [textStorage addLayoutManager:layoutManager];
    
        NSUInteger index = [layoutManager characterIndexForPoint:[gestureRecognizer locationInView:self]
                                                 inTextContainer:textContainer
                        fractionOfDistanceBetweenInsertionPoints:NULL];
        objc_setAssociatedObject(self, &INDEX, @(index), OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save index
    
        return (BOOL)[self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL]; // tapped on part of a link?
    }
    
    - (void)openLink
    {
        NSUInteger index = [objc_getAssociatedObject(self, &INDEX) unsignedIntegerValue]; // retrieve index
        NSURL *url = [self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL];
        if (url && [UIApplication.sharedApplication canOpenURL:url]) [UIApplication.sharedApplication openURL:url];
    }
    
    @end 
    

    This would be a bit cleaner done via a UILabel subclass (ie none of the objc_getAssociatedObject mess), but if you are like me you prefer to avoid having to make unnecessary (3rd party) subclasses just to add some extra function to existing UIKit classes. Also, this has the beauty that it adds clickable-links to any existing UILabel, eg existing UITableViewCells!

    I’ve tried to make it as minimally invasive as possible by using the existing NSLinkAttributeName attribute stuff already available in NSAttributedString. So its a simple as:

    NSURL *myURL = [NSURL URLWithString:@"http://www.google.com"];
    NSMutableAttributedString *myString = [NSMutableAttributedString.alloc initWithString:@"This string has a clickable link: "];
    [myString appendAttributedString:[NSAttributedString.alloc initWithString:@"click here" attributes:@{NSLinkAttributeName:myURL}]];
    ...
    myLabel.attributedText = myString;
    myLabel.enableLinks = YES; // yes, that's all! :-)
    

    Basically, it works by adding a UIGestureRecognizer to your UILabel. The hard work is done in gestureRecognizerShouldBegin:, which re-layouts the attributedText string to find out which character was tapped on. If this character was part of a NSLinkAttributeName then the gestureRecognizer will subsequently fire, retrieve the corresponding URL (from the NSLinkAttributeName value), and open the link per the usual [UIApplication.sharedApplication openURL:url] process.

    Note – by doing all this in gestureRecognizerShouldBegin:, if you dont happen to tap on a link in the label, the event is passed along. So, for example, your UITableViewCell will capture taps on links, but otherwise behave normally (select cell, unselect, scroll, …).

    I’ve put this in a GitHub repository here.
    Adapted from Kai Burghardt’s SO posting here.

    TAGS #Swift2.0

    I take inspiration on – excellent – @NAlexN’s answer and I decide to write by myself a wrapper of UILabel.
    I also tried TTTAttributedLabel but I can’t make it works.

    Hope you can appreciate this code, any suggestions are welcome!

    import Foundation
    
    @objc protocol TappableLabelDelegate {
        optional func tappableLabel(tabbableLabel: TappableLabel, didTapUrl: NSURL, atRange: NSRange)
    }
    
    /// Represent a label with attributed text inside.
    /// We can add a correspondence between a range of the attributed string an a link (URL)
    /// By default, link will be open on the external browser @see 'openLinkOnExternalBrowser'
    
    class TappableLabel: UILabel {
    
        // MARK: - Public properties -
    
        var links: NSMutableDictionary = [:]
        var openLinkOnExternalBrowser = true
        var delegate: TappableLabelDelegate?
    
        // MARK: - Constructors -
    
        override func awakeFromNib() {
            super.awakeFromNib()
            self.enableInteraction()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.enableInteraction()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        private func enableInteraction() {
            self.userInteractionEnabled = true
            self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapOnLabel:")))
        }
    
        // MARK: - Public methods -
    
        /**
        Add correspondence between a range and a link.
    
        - parameter url:   url.
        - parameter range: range on which couple url.
        */
        func addLink(url url: String, atRange range: NSRange) {
            self.links[url] = range
        }
    
        // MARK: - Public properties -
    
        /**
        Action rised on user interaction on label.
    
        - parameter tapGesture: gesture.
        */
        func didTapOnLabel(tapGesture: UITapGestureRecognizer) {
            let labelSize = self.bounds.size;
    
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSizeZero)
            let textStorage = NSTextStorage(attributedString: self.attributedText!)
    
            // configure textContainer for the label
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = self.lineBreakMode
            textContainer.maximumNumberOfLines = self.numberOfLines
            textContainer.size = labelSize;
    
            // configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = tapGesture.locationInView(self)
    
            let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
            let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
            let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                locationOfTouchInLabel.y - textContainerOffset.y)
            let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
                inTextContainer:textContainer,
                fractionOfDistanceBetweenInsertionPoints: nil)
    
            for (url, value) in self.links {
                if let range = value as? NSRange {
                    if NSLocationInRange(indexOfCharacter, range) {
                        let url = NSURL(string: url as! String)!
                        if self.openLinkOnExternalBrowser {
                            UIApplication.sharedApplication().openURL(url)
                        }
                        self.delegate?.tappableLabel?(self, didTapUrl: url, atRange: range)
                    }
                }
            }
        }
    
    }
    

    Drop-in solution as a category on UILabel (this assumes your UILabel uses an attributed string with some NSLinkAttributeName attributes in it):

    @implementation UILabel (Support)
    
    - (BOOL)openTappedLinkAtLocation:(CGPoint)location {
      CGSize labelSize = self.bounds.size;
    
      NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
      textContainer.lineFragmentPadding = 0.0;
      textContainer.lineBreakMode = self.lineBreakMode;
      textContainer.maximumNumberOfLines = self.numberOfLines;
      textContainer.size = labelSize;
    
      NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
      [layoutManager addTextContainer:textContainer];
    
      NSTextStorage* textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
      [textStorage addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, textStorage.length)];
      [textStorage addLayoutManager:layoutManager];
    
      CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
      CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
      CGPoint locationOfTouchInTextContainer = CGPointMake(location.x - textContainerOffset.x, location.y - textContainerOffset.y);
      NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nullptr];
      if (indexOfCharacter >= 0) {
        NSURL* url = [textStorage attribute:NSLinkAttributeName atIndex:indexOfCharacter effectiveRange:nullptr];
        if (url) {
          [[UIApplication sharedApplication] openURL:url];
          return YES;
        }
      }
      return NO;
    }
    
    @end
    
        NSString *string = name;
        NSError *error = NULL;
        NSDataDetector *detector =
        [NSDataDetector dataDetectorWithTypes:(NSTextCheckingTypes)NSTextCheckingTypeLink | NSTextCheckingTypePhoneNumber
                                        error:&error];
        NSArray *matches = [detector matchesInString:string
                                             options:0
                                               range:NSMakeRange(0, [string length])];
        for (NSTextCheckingResult *match in matches)
        {
            if (([match resultType] == NSTextCheckingTypePhoneNumber))
            {
                NSString *phoneNumber = [match phoneNumber];
                NSLog(@" Phone Number is :%@",phoneNumber);
                label.enabledTextCheckingTypes = NSTextCheckingTypePhoneNumber;
            }
    
            if(([match resultType] == NSTextCheckingTypeLink))
            {
                NSURL *email = [match URL];
                NSLog(@"Email is  :%@",email);
                label.enabledTextCheckingTypes = NSTextCheckingTypeLink;
            }
    
            if (([match resultType] == NSTextCheckingTypeLink))
            {
                NSURL *url = [match URL];
                NSLog(@"URL is  :%@",url);
                label.enabledTextCheckingTypes = NSTextCheckingTypeLink;
            }
        }
    
        label.text =name;
    }