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
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:
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.