Is it possible to use the autoshrink property in conjunction on multiple lines on a UILabel
? for example, the large text size possible on 2 available lines.
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 <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.
These people found a solution:
http://www.11pixel.com/blog/28/resize-multi-line-text-to-fit-uilabel-on-iphone/
Their solution is as follows:
int maxDesiredFontSize = 28;
int minFontSize = 10;
CGFloat labelWidth = 260.0f;
CGFloat labelRequiredHeight = 180.0f;
//Create a string with the text we want to display.
self.ourText = @"This is your variable-length string. Assign it any way you want!";
/* This is where we define the ideal font that the Label wants to use.
Use the font you want to use and the largest font size you want to use. */
UIFont *font = [UIFont fontWithName:@"Marker Felt" size:maxDesiredFontSize];
int i;
/* Time to calculate the needed font size.
This for loop starts at the largest font size, and decreases by two point sizes (i=i-2)
Until it either hits a size that will fit or hits the minimum size we want to allow (i > 10) */
for(i = maxDesiredFontSize; i > minFontSize; i=i-2)
{
// Set the new font size.
font = [font fontWithSize:i];
// You can log the size you're trying: NSLog(@"Trying size: %u", i);
/* This step is important: We make a constraint box
using only the fixed WIDTH of the UILabel. The height will
be checked later. */
CGSize constraintSize = CGSizeMake(labelWidth, MAXFLOAT);
// This step checks how tall the label would be with the desired font.
CGSize labelSize = [self.ourText sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
/* Here is where you use the height requirement!
Set the value in the if statement to the height of your UILabel
If the label fits into your required height, it will break the loop
and use that font size. */
if(labelSize.height <= labelRequiredHeight)
break;
}
// You can see what size the function is using by outputting: NSLog(@"Best size is: %u", i);
// Set the UILabel's font to the newly adjusted font.
msg.font = font;
// Put the text into the UILabel outlet variable.
msg.text = self.ourText;
In order to get this working, a IBOutlet must be assigned in the interface builder to the UILabel.
"IBOutlet UILabel *msg;"
All the merit is of the people at 11pixel.
A swifty version adapted from @DaGaMs.
SWIFT 2:
extension UILabel {
func adjustFontSizeToFit(minimumFontSize: CGFloat, maximumFontSize: CGFloat? = nil) {
let maxFontSize = maximumFontSize ?? font.pointSize
for size in stride(from: maxFontSize, to: minimumFontSize, by: -CGFloat(0.1)) {
let proposedFont = font.fontWithSize(size)
let constraintSize = CGSizeMake(bounds.size.width, CGFloat(MAXFLOAT))
let labelSize = ((text ?? "") as NSString).boundingRectWithSize(constraintSize,
options: .UsesLineFragmentOrigin,
attributes: [NSFontAttributeName: proposedFont],
context: nil)
if labelSize.height <= bounds.size.height {
font = proposedFont
setNeedsLayout()
break;
}
}
}
}
SWIFT 3:
extension UILabel {
func adjustFontSizeToFit(minimumFontSize: CGFloat, maximumFontSize: CGFloat? = nil) {
let maxFontSize = maximumFontSize ?? font.pointSize
for size in stride(from: maxFontSize, to: minimumFontSize, by: -CGFloat(0.1)) {
let proposedFont = font.withSize(size)
let constraintSize = CGSize(width: bounds.size.width, height: CGFloat(MAXFLOAT))
let labelSize = ((text ?? "") as NSString).boundingRect(with: constraintSize,
options: .usesLineFragmentOrigin,
attributes: [NSFontAttributeName: proposedFont],
context: nil)
if labelSize.height <= bounds.size.height {
font = proposedFont
setNeedsLayout()
break;
}
}
}
}
Try this:
Either subclass UILabel or call adjustFontSize method after setting the text property on a label
override var text : String? { didSet { self.adjustFontSize() } }
func adjustFontSize()
{
var lineCount = self.string.components(separatedBy: "\n").count - 1
var textArray = self.string.components(separatedBy: " ")
var wordsToCompare = 1
while(textArray.count > 0)
{
let words = textArray.first(n: wordsToCompare).joined(separator: " ")
let wordsWidth = words.widthForHeight(0, font: self.font)
if(wordsWidth > self.frame.width)
{
textArray.removeFirst(wordsToCompare)
lineCount += 1
wordsToCompare = 1
}
else if(wordsToCompare > textArray.count)
{
break
}
else
{
wordsToCompare += 1
}
}
self.numberOfLines = lineCount + 1
}
I found this link http://beckyhansmeyer.com/2015/04/09/autoshrinking-text-in-a-multiline-uilabel/
The problem can be solved using the Interface Builder in 3 simple steps:
Hope it helps!
I cannot comment the post of MontiRabbit due to reputation lacking, so i'll make a new answer. The solution he (and her referrer) proposed do not work on Xcode 7.3 or better, it's imprecise. To make it work, in storyboard, I had to:
Hope it helps! ;)