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.
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.
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:
- Set “Autoshrink” to “Minimum font size.”
- Set the font to your largest desirable font size (20) and set Lines to, say, 10, which in my case was as many lines as would fit in the label at that font size.
- Then, change “Line Breaks” from “Word Wrap” to “Truncate Tail.”
Hope it helps!
I modified the accepted answer's code somewhat to make it a category on UILabel
:
Header file:
#import <UIKit/UIKit.h>
@interface UILabel (MultiLineAutoSize)
- (void)adjustFontSizeToFit;
@end
And the implementation file:
@implementation UILabel (MultiLineAutoSize)
- (void)adjustFontSizeToFit
{
UIFont *font = self.font;
CGSize size = self.frame.size;
for (CGFloat maxSize = self.font.pointSize; maxSize >= self.minimumFontSize; maxSize -= 1.f)
{
font = [font fontWithSize:maxSize];
CGSize constraintSize = CGSizeMake(size.width, MAXFLOAT);
CGSize labelSize = [self.text sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
if(labelSize.height <= size.height)
{
self.font = font;
[self setNeedsLayout];
break;
}
}
// set the font to the minimum size anyway
self.font = font;
[self setNeedsLayout];
}
@end
if
clause are redundant, as they will be executed right under the for
cycle. (2) if there's also need of UILabel's text to be vertically aligned to top (in case it is using fewer lines than actually can be shown in label), it suffice to set label's height to labelSize.height. Note that sizeToFit can not be used for this purpose, as it breaks previously formatted text and sizes it to original font size. –
Barthol float minFontSize = self.font.pointSize * self.minimumScaleFactor;
. Then replace >= self.minimumFontSize
with >= minFontSize
. –
Crazyweed 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.
The answer marked as the solution is hacky and imprecise. UILabel will handle it automatically if you set the following properties correctly:
numberOfLines
must be nonzero
adjustsFontSizeToFitWidth
must be YES
lineBreakMode
must not be NSLineBreakByCharWrapping
or NSLineBreakByWordWrapping
Here's the category solution updated to iOS 7 based off of itecedor's updates for iOS 6.
Header file:
#import <UIKit/UIKit.h>
@interface UILabel (MultiLineAutoSize)
- (void)adjustFontSizeToFit;
@end
And the implementation file:
@implementation UILabel (MultiLineAutoSize)
- (void)adjustFontSizeToFit {
UIFont *font = self.font;
CGSize size = self.frame.size;
for (CGFloat maxSize = self.font.pointSize; maxSize >= self.minimumScaleFactor * self.font.pointSize; maxSize -= 1.f)
{
font = [font fontWithSize:maxSize];
CGSize constraintSize = CGSizeMake(size.width, MAXFLOAT);
CGRect textRect = [self.text boundingRectWithSize:constraintSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:font}
context:nil];
CGSize labelSize = textRect.size;
if(labelSize.height <= size.height)
{
self.font = font;
[self setNeedsLayout];
break;
}
}
// set the font to the minimum size anyway
self.font = font;
[self setNeedsLayout]; }
@end
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:
- Set a width constraint (pure width or tail&lead)
- SET an HEIGHT CONSTRAINT (this is very important, normally with autoresize one does not set the label height)
- Set "Autoshrink" property to "Minimum font scale" or "Minimum font size" (works in both cases)
- Set "Line Breaks" property to "Truncate Tail"
- Set "Lines" property to a non-zero value
Hope it helps! ;)
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;
}
}
}
}
i liked DaGaMs's answer, but in using labels like in UITableViewCells that could be returned of dequeueReusableCell:, the regular font size would continue to shrink even as the original font size was still desired for some tableView cells that had less text and could take advantage of the original label's original font size.
so, i starting with DaGaMs's category as a jumping off point, i created a separate class rather than a separate category, and i make sure my UILabels in my storyboard make use of this new class:
#import "MultiLineAutoShrinkLabel.h"
@interface MultiLineAutoShrinkLabel ()
@property (readonly, nonatomic) UIFont* originalFont;
@end
@implementation MultiLineAutoShrinkLabel
@synthesize originalFont = _originalFont;
- (UIFont*)originalFont { return _originalFont ? _originalFont : (_originalFont = self.font); }
- (void)quoteAutoshrinkUnquote
{
UIFont* font = self.originalFont;
CGSize frameSize = self.frame.size;
CGFloat testFontSize = _originalFont.pointSize;
for (; testFontSize >= self.minimumFontSize; testFontSize -= 0.5)
{
CGSize constraintSize = CGSizeMake(frameSize.width, MAXFLOAT);
CGSize testFrameSize = [self.text sizeWithFont:(font = [font fontWithSize:testFontSize])
constrainedToSize:constraintSize
lineBreakMode:self.lineBreakMode];
// the ratio of testFontSize to original font-size sort of accounts for number of lines
if (testFrameSize.height <= frameSize.height * (testFontSize/_originalFont.pointSize))
break;
}
self.font = font;
[self setNeedsLayout];
}
@end
- (void)quoteAutoshrinkUnquote;
… and actually, i've moved on to use something similar but a bit more complicated that deals with the accessoryView on the right side and the fact that it can be 2 different sizes, allowing for more or less screen real estate per cell. –
Kelton itedcedor's answer has an issue that pwightman pointed out. Also, there is no need to trim whitespaces. Here it is the modified version:
- (void)adjustFontSizeToFit {
UIFont *font = self.font;
CGSize size = self.frame.size;
for (CGFloat maxSize = self.font.pointSize; maxSize >= self.minimumScaleFactor * self.font.pointSize; maxSize -= 1.f) {
font = [font fontWithSize:maxSize];
CGSize constraintSize = CGSizeMake(size.width, MAXFLOAT);
CGSize labelSize = [self.text sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:NSLineBreakByWordWrapping];
if(labelSize.height <= size.height) {
self.font = font;
[self setNeedsLayout];
break;
}
}
// set the font to the minimum size anyway
self.font = font;
[self setNeedsLayout];
}
Thank you to DaGaMs for this solution.
I've updated it as follows:
1 - To work with iOS 6 (since both minimumFontSize and UILineBreakModeWordWrap are deprecated) 2 - To strip whitespace from the label's text, as it will cause the resizing to fail (you don't want to know how long it took me to find that bug)
-(void)adjustFontSizeToFit
{
self.text = [self.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
UIFont *font = self.font;
CGSize size = self.frame.size;
for (CGFloat maxSize = self.font.pointSize; maxSize >= self.minimumScaleFactor; maxSize -= 1.f)
{
font = [font fontWithSize:maxSize];
CGSize constraintSize = CGSizeMake(size.width, MAXFLOAT);
CGSize labelSize = [self.text sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:NSLineBreakByWordWrapping];
if(labelSize.height <= size.height)
{
self.font = font;
[self setNeedsLayout];
break;
}
}
// set the font to the minimum size anyway
self.font = font;
[self setNeedsLayout];
}
minimumScaleFactor
is some decimal value, like 0.2. maxSize >= self.minimumScaleFactor
would then reduce the size to 0.2. Instead, would you not want maxSize >= self.minimumScaleFactor * self.font.pointSize
or something similar? –
Angularity I used @wbarksdale's Swift 3 solution but found that long words were being truncated in the middle. To keep words intact, I had to modify as shown:
extension UILabel {
func adjustFontSizeToFit(minimumFontSize: CGFloat, maximumFontSize: CGFloat? = nil) {
let maxFontSize = maximumFontSize ?? font.pointSize
let words = self.text?.components(separatedBy: " ")
var longestWord: String?
if let max = words?.max(by: {$1.characters.count > $0.characters.count}) {
longestWord = max
}
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)
let wordConstraintSize = CGSize(width: CGFloat(MAXFLOAT), height: CGFloat(MAXFLOAT))
let longestWordSize = ((longestWord ?? "") as NSString).boundingRect(with: wordConstraintSize,
options: .usesFontLeading,
attributes: [NSFontAttributeName: proposedFont],
context: nil)
if labelSize.height <= bounds.size.height && longestWordSize.width < constraintSize.width {
font = proposedFont
setNeedsLayout()
break
}
}
}
}
UILabel
). I just changed the separator from a space to a newline ("\n") and deleted all things related to height
. Thank you for this! –
Platform For UIButton, just these lines are working for me:
self.centerBtn.titleLabel.numberOfLines = 2;
self.centerBtn.titleLabel.textAlignment = NSTextAlignmentCenter;
self.centerBtn.titleLabel.adjustsFontSizeToFitWidth = YES;
I've written a small category on UILabel based on "The Dude's" answer above to achieve this functionality.
NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin
to make it work. –
Flexile 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.
There is a method on NSString
, -sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode:
which has apparently existed since iOS 2.0, but unfortunately is deprecated in iOS 7 without a suggested alternative as the automatic reduction of font size is discouraged. I don't really understand Apple's stance on this as they use it in keynote etc and I think if the font sizes are within a small range it is ok. Here's an implementation in Swift using this method.
var newFontSize: CGFloat = 30
let font = UIFont.systemFontOfSize(newFontSize)
(self.label.text as NSString).sizeWithFont(font, minFontSize: 20, actualFontSize: &newFontSize, forWidth: self.label.frame.size.width, lineBreakMode: NSLineBreakMode.ByWordWrapping)
self.label.font = font.fontWithSize(newFontSize)
I'm not aware of a way this can be achieved without using deprecated methods.
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
}
extension UILabel{
func adjustFont(minSize:Int, maxSize:Int){
var newFont = self.font
for index in stride(from: maxSize, to: minSize, by: -1) {
newFont = UIFont.systemFont(ofSize: CGFloat(index))
let size = CGSize(width: self.frame.width, height: CGFloat(Int.max))
let size2 = (self.text! as NSString).boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedStringKey.font:newFont!], context: nil).size
if size2.height < self.frame.size.height{
break
}
}
self.font = newFont
}
}
you need to assign value to the numberOfLines property of UILabel as well.
© 2022 - 2024 — McMap. All rights reserved.