Autoshrink on a UILabel with multiple lines

IosIos5Uilabel

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

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

Solution 2 - Ios

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:

  1. Set “Autoshrink” to “Minimum font size.”
  2. 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.
  3. Then, change “Line Breaks” from “Word Wrap” to “Truncate Tail.”

Hope it helps!

Solution 3 - Ios

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.

Solution 4 - Ios

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

Solution 5 - Ios

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

Solution 6 - Ios

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:

  1. Set a width constraint (pure width or tail&lead)
  2. SET an HEIGHT CONSTRAINT (this is very important, normally with autoresize one does not set the label height)
  3. Set "Autoshrink" property to "Minimum font scale" or "Minimum font size" (works in both cases)
  4. Set "Line Breaks" property to "Truncate Tail"
  5. Set "Lines" property to a non-zero value

Hope it helps! ;)

Solution 7 - Ios

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;
            }
        }
    }
}

Solution 8 - Ios

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

Solution 9 - Ios

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];
}

Solution 10 - Ios

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];
}

Solution 11 - Ios

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
            }
        }
    }
}

Solution 12 - Ios

For UIButton, just these lines are working for me:

self.centerBtn.titleLabel.numberOfLines = 2;
self.centerBtn.titleLabel.textAlignment = NSTextAlignmentCenter;
self.centerBtn.titleLabel.adjustsFontSizeToFitWidth = YES;

Solution 13 - Ios

I've written a small category on UILabel based on "The Dude's" answer above to achieve this functionality.

https://gist.github.com/ayushn21/d87b835b2efc756e859f

Solution 14 - Ios

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

enter image description here

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.

Solution 15 - Ios

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.

Solution 16 - Ios

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
}

Solution 17 - Ios

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjfiskView Question on Stackoverflow
Solution 1 - IosDaGaMsView Answer on Stackoverflow
Solution 2 - IosMontiRabbitView Answer on Stackoverflow
Solution 3 - IosThe dudeView Answer on Stackoverflow
Solution 4 - IosstevenpaulrView Answer on Stackoverflow
Solution 5 - IosGoldenJoeView Answer on Stackoverflow
Solution 6 - IosNemView Answer on Stackoverflow
Solution 7 - IoswfbarksdaleView Answer on Stackoverflow
Solution 8 - Iosjohn.k.doeView Answer on Stackoverflow
Solution 9 - IosAbrasView Answer on Stackoverflow
Solution 10 - IositecedorView Answer on Stackoverflow
Solution 11 - IosDaniel McLeanView Answer on Stackoverflow
Solution 12 - IosGeman WuView Answer on Stackoverflow
Solution 13 - Iosayushn21View Answer on Stackoverflow
Solution 14 - IosJoseph AstrahanView Answer on Stackoverflow
Solution 15 - IosNickView Answer on Stackoverflow
Solution 16 - IosRendelView Answer on Stackoverflow
Solution 17 - IoscnuView Answer on Stackoverflow