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

前端 未结 30 2666
半阙折子戏
半阙折子戏 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 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) 
    @property BOOL enableLinks;
    @end
    
    #import 
    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.

提交回复
热议问题