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

前端 未结 30 2593
半阙折子戏
半阙折子戏 2020-11-22 02:07

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

相关标签:
30条回答
  • 2020-11-22 02:59

    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];
    }
    
    0 讨论(0)
  • 2020-11-22 03:00

    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.

    0 讨论(0)
  • 2020-11-22 03:03

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

    0 讨论(0)
  • 2020-11-22 03:03

    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];
    
    0 讨论(0)
  • 2020-11-22 03:06

    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.location(in: label)
            let textBoundingBox = layoutManager.usedRect(for: textContainer)
            let textContainerOffset = CGPoint(
                x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y
            )
            let locationOfTouchInTextContainer = CGPoint(
                x: locationOfTouchInLabel.x - textContainerOffset.x,
                y: locationOfTouchInLabel.y - textContainerOffset.y
            )
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: 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")
        }
    }
    

    IMPORTANT: The UILabel line break mode must be set to wrap by word/char. Somehow, NSTextContainer will assume that the text is single line only if the line break mode is otherwise.

    0 讨论(0)
  • 2020-11-22 03:07

    (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);
    
    }
    
    0 讨论(0)
提交回复
热议问题