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

前端 未结 30 2477
半阙折子戏
半阙折子戏 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: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 with your actual label name):

    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 with your label name again:

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

    @IBAction func tapLabel(gesture: UITapGestureRecognizer) {
            guard let text = 

    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.

提交回复
热议问题