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

前端 未结 30 2479
半阙折子戏
半阙折子戏 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:50

    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.

    0 讨论(0)
  • 2020-11-22 02:51

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

    I found a other solution:

    I find a way to detect the link in a html text that you find from the internet you transform it into nsattributeString with :

    func htmlAttributedString(fontSize: CGFloat = 17.0) -> NSAttributedString? {
                let fontName = UIFont.systemFont(ofSize: fontSize).fontName
                let string = self.appending(String(format: "<style>body{font-family: '%@'; font-size:%fpx;}</style>", fontName, fontSize))
                guard let data = string.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
    
                guard let html = try? NSMutableAttributedString (
                    data: data,
                    options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
                    documentAttributes: nil) else { return nil }
                return html
            }
    

    My method allows you to detect the hyperlink without having to specify them.

    • first you create an extension of the tapgesturerecognizer :

      extension UITapGestureRecognizer {
      func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
          guard let attrString = label.attributedText else {
              return false
          }
      
          let layoutManager = NSLayoutManager()
          let textContainer = NSTextContainer(size: .zero)
          let textStorage = NSTextStorage(attributedString: attrString)
      
          layoutManager.addTextContainer(textContainer)
          textStorage.addLayoutManager(layoutManager)
      
          textContainer.lineFragmentPadding = 0
          textContainer.lineBreakMode = label.lineBreakMode
          textContainer.maximumNumberOfLines = label.numberOfLines
          let labelSize = label.bounds.size
          textContainer.size = labelSize
      
          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)
      }
      

      }

    then in you view controller you created a list of url and ranges to store all the links and the range that the attribute text contain:

    var listurl : [String] = []
        var listURLRange : [NSRange] = []
    

    to find the URL and the URLRange you can use :

        fun findLinksAndRange(attributeString : NSAttributeString){
            notification.enumerateAttribute(NSAttributedStringKey.link , in: NSMakeRange(0, notification.length), options: [.longestEffectiveRangeNotRequired]) { value, range, isStop in
                        if let value = value {
                            print("\(value) found at \(range.location)")
                            let stringValue = "\(value)"
                            listurl.append(stringValue)
                            listURLRange.append(range)
                        }
                    }
    
                westlandNotifcationLabel.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(handleTapOnLabel(_:))))
    
        }
    

    then you implementing the handle tap :

    @objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
            for index in 0..<listURLRange.count{
                if recognizer.didTapAttributedTextInLabel(label: westlandNotifcationLabel, inRange: listURLRange[index]) {
                    goToWebsite(url : listurl[index])
                }
            }
        }
    
        func goToWebsite(url : String){
            if let websiteUrl = URL(string: url){
                if #available(iOS 10, *) {
                    UIApplication.shared.open(websiteUrl, options: [:],
                                              completionHandler: {
                                                (success) in
                                                print("Open \(websiteUrl): \(success)")
                    })
                } else {
                    let success = UIApplication.shared.openURL(websiteUrl)
                    print("Open \(websiteUrl): \(success)")
                }
            }
        }
    

    and here we go!

    I hope this solution help you like it help me.

    0 讨论(0)
  • 2020-11-22 02:51

    Yes this is possible albeit very confusing to figure out at first. I will go a step further and show you how you can even click on any area in the text as well.

    With this method you can have UI Label tha is:

    • Multiline Friendly
    • Autoshrink Friendly
    • Clickable Friendly (yes, even individual characters)
    • Swift 5

    Step 1:

    Make the UILabel have the properties for Line Break of 'Truncate Tail' and set a minimum font scale.

    If you are unfamiliar with font scale just remember this rule:

    minimumFontSize/defaultFontSize = fontscale

    In my case I wanted 7.2 to be the minimum font size and my starting font size was 36. Therefore, 7.2 / 36 = 0.2

    Step 2:

    If you do not care about the labels being clickable and just wanted a working multiline label you are done!

    HOWEVER, if you want the labels to be clickable read on...

    Add this following extension I created

    extension UILabel {
    
        func setOptimalFontSize(maxFontSize:CGFloat,text:String){
            let width = self.bounds.size.width
    
            var font_size:CGFloat = maxFontSize //Set the maximum font size.
            var stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
            while(stringSize.width > width){
                font_size = font_size - 1
                stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
            }
    
            self.font = self.font.withSize(font_size)//Forcefully change font to match what it would be graphically.
        }
    }
    

    It's used like this (just replace <Label> with your actual label name):

    <Label>.setOptimalFontSize(maxFontSize: 36.0, text: formula)
    

    This extension is needed because auto shrink does NOT change the 'font' property of the label after it auto-shrinks so you have to deduce it by calculating it the same way it does by using .size(withAttributes) function which simulates what it's size would be with that particular font.

    This is necessary because the solution for detecting where to click on the label requires the exact font size to be known.

    Step 3:

    Add the following extension:

    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 mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
            mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
    
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = 6
            paragraphStyle.lineBreakMode = .byTruncatingTail
            paragraphStyle.alignment = .center
            mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
    
            let textStorage = NSTextStorage(attributedString: mutableAttribString)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
    
            textStorage.addLayoutManager(layoutManager)
    
            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 = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                  //(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
            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 = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                            // locationOfTouchInLabel.y - textContainerOffset.y);
            let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
    
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            print("IndexOfCharacter=",indexOfCharacter)
    
            print("TargetRange=",targetRange)
            return NSLocationInRange(indexOfCharacter, targetRange)
        }
    
    }
    

    You will need to modify this extension for your particular multiline situation. In my case you will notice that I use a paragraph style.

    let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = 6
            paragraphStyle.lineBreakMode = .byTruncatingTail
            paragraphStyle.alignment = .center
            mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
    

    Make sure to change this in the extension to what you are actually using for your line spacing so that everything calculates correctly.

    Step 4:

    Add the gestureRecognizer to the label in viewDidLoad or where you think is appropriate like so (just replace <Label> with your label name again:

    <Label>.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
    

    Here is a simplified example of my tapLabel function (just replace <Label> with your UILabel name):

    @IBAction func tapLabel(gesture: UITapGestureRecognizer) {
            guard let text = <Label>.attributedText?.string else {
                return
            }
    
            let click_range = text.range(of: "(α/β)")
    
            if gesture.didTapAttributedTextInLabel(label: <Label>, inRange: NSRange(click_range!, in: text)) {
               print("Tapped a/b")
            }else {
               print("Tapped none")
            }
        }
    

    Just a note in my example, my string is BED = N * d * [ RBE + ( d / (α/β) ) ], so I was just getting the range of the α/β in this case. You could add "\n" to the string to add a newline and whatever text you wanted after and test this to find a string on the next line and it will still find it and detect the click correctly!

    That's it! You are done. Enjoy a multiline clickable label.

    0 讨论(0)
  • 2020-11-22 02:52

    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/"]];
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:52

    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.

    0 讨论(0)
提交回复
热议问题