65
votes

I've seen a bunch of examples for changing the size of a UILabel.

Here's what I'd like to do: Change the font size so that the text will be as large as possible within the new height.

Any clues?

21
I've just realized that performing a binary or other search is completely unnecessary. You need only iterate (a couple of times) using a ratio search. it is dead easy. I will paste in full code as an answer.Fattie
.. compete solution for 2016 stackoverflow.com/a/37277874/294884 Very straightforward code.Fattie

21 Answers

42
votes

I had the very same problem and, thanks to this thread and Joel's algorithm, I could fix it. :-)

Below is my code in Swift. I'm in iOS 8 + Autolayout.

Problem:

  1. User inputs expenses:

123 app

  1. When users tap the 'check' button, a menu appears from bottom, pushing everything to the top of the screen (shrinking stuff, including the label):

123 app

After the fix:

123 app

Which is exactly what the designer had in mind... :)

xScope app :)

I subclassed UILabel and overrode layoutSubviews. Then each time the UILabel gets its size changed, the font size is recalculated:

//
//  LabelWithAdaptiveTextHeight.swift
//  123
//
//  Created by https://github.com/backslash-f on 12/19/14.
//

/*
 Designed with single-line UILabels in mind, this subclass 'resizes' the label's text (it changes the label's font size)
 everytime its size (frame) is changed. This 'fits' the text to the new height, avoiding undesired text cropping.
 Kudos to this Stack Overflow thread: bit.ly/setFontSizeToFillUILabelHeight
*/

import Foundation
import UIKit

class LabelWithAdaptiveTextHeight: UILabel {

    override func layoutSubviews() {
        super.layoutSubviews()
        font = fontToFitHeight()
    }

    // Returns an UIFont that fits the new label's height.
    private func fontToFitHeight() -> UIFont {

        var minFontSize: CGFloat = DISPLAY_FONT_MINIMUM // CGFloat 18
        var maxFontSize: CGFloat = DISPLAY_FONT_BIG     // CGFloat 67
        var fontSizeAverage: CGFloat = 0
        var textAndLabelHeightDiff: CGFloat = 0

        while (minFontSize <= maxFontSize) {

            fontSizeAverage = minFontSize + (maxFontSize - minFontSize) / 2

            // Abort if text happens to be nil
            guard text?.characters.count > 0 else {
              break
            }

            if let labelText: NSString = text {
                let labelHeight = frame.size.height

                let testStringHeight = labelText.sizeWithAttributes(
                    [NSFontAttributeName: font.fontWithSize(fontSizeAverage)]
                ).height

                textAndLabelHeightDiff = labelHeight - testStringHeight

                if (fontSizeAverage == minFontSize || fontSizeAverage == maxFontSize) {
                    if (textAndLabelHeightDiff < 0) {
                        return font.fontWithSize(fontSizeAverage - 1)
                    }
                    return font.fontWithSize(fontSizeAverage)
                }

                if (textAndLabelHeightDiff < 0) {
                    maxFontSize = fontSizeAverage - 1

                } else if (textAndLabelHeightDiff > 0) {
                    minFontSize = fontSizeAverage + 1

                } else {
                    return font.fontWithSize(fontSizeAverage)
                }
            }
        }
        return font.fontWithSize(fontSizeAverage)
    }
}

36
votes

There is a simpler solution. Just add below lines and magically, the label adjusts its font size to fit the height of the label too:

SWIFT 3:

label.minimumScaleFactor = 0.1    //or whatever suits your need
label.adjustsFontSizeToFitWidth = true    
label.lineBreakMode = .byClipping
label.numberOfLines = 0
35
votes

Here's how I did it, since DGund's answer didn't work for me, it fit the width, but I wanted it to fit the height.

+ (UIFont *)findAdaptiveFontWithName:(NSString *)fontName forUILabelSize:(CGSize)labelSize withMinimumSize:(NSInteger)minSize
{
    UIFont *tempFont = nil;
    NSString *testString = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    NSInteger tempMin = minSize;
    NSInteger tempMax = 256;
    NSInteger mid = 0;
    NSInteger difference = 0;

    while (tempMin <= tempMax) {
        mid = tempMin + (tempMax - tempMin) / 2;
        tempFont = [UIFont fontWithName:fontName size:mid];
        difference = labelSize.height - [testString sizeWithFont:tempFont].height;

        if (mid == tempMin || mid == tempMax) {
            if (difference < 0) {
                return [UIFont fontWithName:fontName size:(mid - 1)];
            }

            return [UIFont fontWithName:fontName size:mid];
        }

        if (difference < 0) {
            tempMax = mid - 1;
        } else if (difference > 0) {
            tempMin = mid + 1;
        } else {
            return [UIFont fontWithName:fontName size:mid];
        }
    }

    return [UIFont fontWithName:fontName size:mid];
}

This will take a font name, a size (it doesn't have to be a UILabel, theoretically, but I always used it with a UILabel), and a minimum size (you could also use a max size, just replace the 256 with the max size parameter). This will essentially test every font size between the minimum and maximum font sizes and return the one that is at or just underneath the target height.

Usage is self explanatory, but looks like this:

self.myLabel.font = [self findAdaptiveFontWithName:@"HelveticaNeue-UltraLight" forUILabelSize:self.myLabel.frame.size withMinimumSize:30];

You can also make this a class method category on UIFont (which is what I did).

EDIT: On suggestion, I removed the for loop and spent a little time making it more efficient with a Binary Search routine. I did several checks to make absolutely sure that the font will end up fitting within the label. In initial testing it appears to work.

24
votes

Edit: Check out Joel Fischer's great answer to programmatically obtain the correct size!

You can set the font to automatically fill the size of a label, and optionally not go below a minimum font size. Just set adjustsFontSizeToFitWidth to YES. Check out the UILabel Class Reference if you need more information.

Although the boolean is called "adjustsFontSizeToFitWidth," it really means the largest size for the height of the label, that will stay on one line of the label (or however many lines you specify).

14
votes

to adapt the text according to the height of my label I have adapt Joel method to swift

func optimisedfindAdaptiveFontWithName(fontName:String, label:UILabel!, minSize:CGFloat,maxSize:CGFloat) -> UIFont!
{

    var tempFont:UIFont
    var tempHeight:CGFloat
    var tempMax:CGFloat = maxSize
    var tempMin:CGFloat = minSize

    while (ceil(tempMin) != ceil(tempMax)){
        let testedSize = (tempMax + tempMin) / 2


        tempFont = UIFont(name:fontName, size:testedSize)
        let attributedString = NSAttributedString(string: label.text!, attributes: [NSFontAttributeName : tempFont])

        let textFrame = attributedString.boundingRectWithSize(CGSize(width: label.bounds.size.width, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin , context: nil)

        let difference = label.frame.height - textFrame.height
        println("\(tempMin)-\(tempMax) - tested : \(testedSize) --> difference : \(difference)")
        if(difference > 0){
            tempMin = testedSize
        }else{
            tempMax = testedSize
        }
    }


    //returning the size -1 (to have enought space right and left)
    return UIFont(name: fontName, size: tempMin - 1)
}

and I use it this way :

myLabel.font = optimisedfindAdaptiveFontWithName("Helvetica", label: myLabel, minSize: 10, maxSize: 38)
    println("\(myLabel.font)")
9
votes

One line called in viewWillAppear does the trick:

testLabel.font = testLabel.font.fontWithSize(testLabel.frame.height * 2/3)

In storyboard, I set all of my label heights relative to the overall height of the view, and this allows the font size to scale dynamically with them.

Notice that the font size is actually 2/3 the height of the label. If the font you are using has tails that dip below the line (as in y, g, q, p, or j), you will want to make the font size a ratio of the label height so that those tails aren't chopped off. 2/3 works well for Helvetica Neue, but try other ratios depending on the font you're using. For fonts without tails, numbers, or all-caps text, a 1:1 ratio may suffice.

9
votes

Good news,

Performing a binary search is completely unnecessary!

You need only iterate (a couple of times) using a ratio search.

        guess = guess * ( desiredHeight / guessHeight )

Here's a full total IBDesignable solution.

Note: when working with designers or typographers, you will need to set the tracking / stretching for fonts. (It's absurd Apple do not include this.) StyledLabel also includes tracking / stretching.

StyledLabel.swift

It sets tracking, stretching, AND it sets the point size to match the view frame height on all devices.

In storyboard: just make the frame of the UILabel, the height you want the text to be - end of story!

// the call fontToFitHeight FINDS THE POINT SIZE TO "FILL TO HEIGHT".
// Just use autolayout to make the frame THE ACTUAL HEIGHT
// you want the type ON ANY DEVICE

// ADDITIONALLY you can set:
// the tracking (that's the overall amount of space between all letters)
// and streching (actually squeeze or stretch the letters horizontally)

// Note: tracking and stretching IS SHOWN IN STORYBOARD LIVE
// WTT crazyrems http://stackoverflow.com/a/37300130/294884

import UIKit

@IBDesignable
class StyledLabel: UILabel
    {
    @IBInspectable var tracking:CGFloat = 0.8
    // values between about 0.7 to 1.3.  one means normal.
    
    @IBInspectable var stretching:CGFloat = -0.1
    // values between about -.5 to .5.  zero means normal.
    
    override func awakeFromNib()
        {
        tweak()
        }
    
    override func prepareForInterfaceBuilder()
        {
        tweak()
        }
    
    override func layoutSubviews()
        {
        super.layoutSubviews()
        font = fontToFitHeight()
        }
    
    private func fontToFitHeight() -> UIFont
        {
/* Apple have failed to include a basic thing needed in handling text: fitting the text to the height. Here's the simplest and fastest way to do that:

        guess = guess * ( desiredHeight / guessHeight )

That's really all there is to it. The rest of the code in this routine is safeguards. Further, the routine iterates a couple of times, which is harmless, to take care of any theoretical bizarre nonlinear sizing issues with strange typefaces. */
        
        guard text?.characters.count > 0 else { return font }
        let desiredHeight:CGFloat = frame.size.height
        guard desiredHeight>1 else { return font }
        var guess:CGFloat
        var guessHeight:CGFloat
        
        print("searching for... ", desiredHeight)
        
        guess = font.pointSize
        if (guess>1&&guess<1000) { guess = 50 }
        
        guessHeight = sizeIf(guess)
        
        if (guessHeight==desiredHeight)
            {
            print("fluke, exact match within float math limits, up front")
            return font.fontWithSize(guess)
            }
        
        var iterations:Int = 4
        
/* It is incredibly unlikely you would need more than four iterations, "two" would rarely be needed. You could imagine some very strange glyph handling where the relationship is non-linear (or something weird): That is the only theoretical reason you'd ever need more than one or two iterations. Note that when you watch the output of the iterations, you'll sometimes/often see same or identical values for the result: this is correct and expected in a float iteration. */
        
        while(iterations>0)
            {
            guess = guess * ( desiredHeight / guessHeight )
            guessHeight = sizeIf(guess)
            
            if (guessHeight==desiredHeight)
                {
                print("unbelievable fluke, exact match within float math limits while iterating")
                return font.fontWithSize(guess)
                }
            
            iterations -= 1
            }
        
        print("done. Shame Apple doesn't do this for us!")
        return font.fontWithSize(guess)
        }
    
    private func sizeIf(pointSizeToTry:CGFloat)->(CGFloat)
        {
        let s:CGFloat = text!.sizeWithAttributes(
            [NSFontAttributeName: font.fontWithSize(pointSizeToTry)] )
            .height
        
        print("guessing .. ", pointSizeToTry, " .. " , s)
        return s
        }
    
    private func tweak()
        {
        let ats = NSMutableAttributedString(string: self.text!)
        let rg = NSRange(location: 0, length: self.text!.characters.count)
        
        ats.addAttribute(
            NSKernAttributeName, value:CGFloat(tracking), range:rg )
                    
        ats.addAttribute(
            NSExpansionAttributeName, value:CGFloat(stretching), range:rg )
        
        self.attributedText = ats
        }
    }
6
votes

Based on @Conaaando's great answer, I've updated it to a version with IBDesignable parameters included, which makes it possible to edit it throughout the Interface builder:

enter image description here

And the code:

//
//  TIFFitToHeightLabel.swift
//

import Foundation
import UIKit

@IBDesignable class TIFFitToHeightLabel: UILabel {

    @IBInspectable var minFontSize:CGFloat = 12 {
        didSet {
            font = fontToFitHeight()
        }
    }

    @IBInspectable var maxFontSize:CGFloat = 30 {
        didSet {
            font = fontToFitHeight()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        font = fontToFitHeight()
    }

    // Returns an UIFont that fits the new label's height.
    private func fontToFitHeight() -> UIFont {

        var minFontSize: CGFloat = self.minFontSize
        var maxFontSize: CGFloat = self.maxFontSize
        var fontSizeAverage: CGFloat = 0
        var textAndLabelHeightDiff: CGFloat = 0

        while (minFontSize <= maxFontSize) {
            fontSizeAverage = minFontSize + (maxFontSize - minFontSize) / 2

            if let labelText: NSString = text {
                let labelHeight = frame.size.height

                let testStringHeight = labelText.sizeWithAttributes(
                    [NSFontAttributeName: font.fontWithSize(fontSizeAverage)]
                    ).height

                textAndLabelHeightDiff = labelHeight - testStringHeight

                if (fontSizeAverage == minFontSize || fontSizeAverage == maxFontSize) {
                    if (textAndLabelHeightDiff < 0) {
                        return font.fontWithSize(fontSizeAverage - 1)
                    }
                    return font.fontWithSize(fontSizeAverage)
                }

                if (textAndLabelHeightDiff < 0) {
                    maxFontSize = fontSizeAverage - 1

                } else if (textAndLabelHeightDiff > 0) {
                    minFontSize = fontSizeAverage + 1

                } else {
                    return font.fontWithSize(fontSizeAverage)
                }
            }
        }
        return font.fontWithSize(fontSizeAverage)
    }
}
4
votes

This borrows heavily from Joel Fischer's answer. His answer takes into account label height only -- I've made some changes to take into account label width as well (given an input string), which I wanted:

typedef enum
{
    kDimensionHeight,
    kDimensionWidth,
} DimensionType;

@implementation UIFont (AdaptiveFont)

+ (UIFont *)_adaptiveFontWithName:(NSString *)fontName minSize:(NSInteger)minSize labelDimension:(CGFloat)labelDimension testString:(NSString *)testString dimension:(DimensionType)dimension
{
    UIFont *tempFont = nil;
    NSInteger tempMin = minSize;
    NSInteger tempMax = 256;
    NSInteger mid = 0;
    NSInteger difference = 0;
    CGFloat testStringDimension = 0.0;

    while (tempMin <= tempMax) {
        @autoreleasepool {
            mid = tempMin + (tempMax - tempMin) / 2;
            tempFont = [UIFont fontWithName:fontName size:mid];

            // determine dimension to test
            if (dimension == kDimensionHeight) {
                testStringDimension = [testString sizeWithFont:tempFont].height;
            } else {
                testStringDimension = [testString sizeWithFont:tempFont].width;
            }
            difference = labelDimension - testStringDimension;

            if (mid == tempMin || mid == tempMax) {
                if (difference < 0) {
                    return [UIFont fontWithName:fontName size:(mid - 1)];
                }
                return [UIFont fontWithName:fontName size:mid];
            }

            if (difference < 0) {
                tempMax = mid - 1;
            } else if (difference > 0) {
                tempMin = mid + 1;
            } else {
                return [UIFont fontWithName:fontName size:mid];
            }
        }
    }
    return [UIFont fontWithName:fontName size:mid];
}

+ (UIFont *)adaptiveFontWithName:(NSString *)fontName minSize:(NSInteger)minSize labelSize:(CGSize)labelSize string:(NSString *)string
{
    UIFont *adaptiveFont = nil;
    NSString *testString = nil;

    // get font, given a max height
    testString = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    UIFont *fontConstrainingHeight = [UIFont _adaptiveFontWithName:fontName minSize:minSize labelDimension:labelSize.height testString:testString dimension:kDimensionHeight];
    CGSize boundsConstrainingHeight = [string sizeWithFont:fontConstrainingHeight];
    CGSize boundsConstrainingWidth = CGSizeZero;

    // if WIDTH is fine (while constraining HEIGHT), return that font
    if (boundsConstrainingHeight.width <= labelSize.width) {
        adaptiveFont = fontConstrainingHeight;
    } else {
        // get font, given a max width
        // i.e., fontConstrainingWidth
        testString = string;
        adaptiveFont = [UIFont _adaptiveFontWithName:fontName minSize:minSize labelDimension:labelSize.width testString:testString dimension:kDimensionWidth];

        // TEST comparison
        boundsConstrainingWidth = [string sizeWithFont:adaptiveFont];
    }
    return adaptiveFont;
}
4
votes

Combining answers by @DGund and @Kashif, here's a simple IB solution:

enter image description here

This fits text by height as low as you specify in Autoshrink parameter.

3
votes

There is a much simpler way to do it. Just calculate point per pixel of the screen and multiply it to the height of your label, and you'll get the desiered font size.
Here are custom methods for this. Choose whatever you want.

TYPE 1. Hardoded single-line version:

- (CGFloat) fontSizeFromHeight:(CGFloat)height
{
     return ceilf(height * (10.0 / [@"Tg" sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:10.0]}].height));
}

TYPE 2. Cleaner version:

- (CGFloat)fontSizeFromHeight:(CGFloat)height
{
    static CGFloat const testFontSize = 12.0;
    static NSString * const testText = @"TestString";
    UIFont *testFont = [UIFont systemFontOfSize:testFontSize];
    CGFloat pixelHeight = [testText sizeWithAttributes:@{NSFontAttributeName:testFont}].height;
    CGFloat pointPerPixel = testFontSize / pixelHeight;
    CGFloat desiredFontSize = ceilf(height * pointPerPixel);
    return desiredFontSize;
}

Usage examples:

myLabel.font = [UIFont systemFontOfSize:[self fontSizeFromHeight:myLabel.frame.size.height]];  
myLabel.font = [myLabel.font fontWithSize:[self fontSizeFromHeight:myLabel.frame.size.height]];
2
votes

Expanding on @Joe Blow's answer, here is an Objective-C category UILabel+FitToHeight which allows you to easily import and toggle a adjustsFontSizeToFitHeight much like you can already adjustsFontSizeToFitWidth.

UILabel+FitToHeight.h

#import <UIKit/UIKit.h>

@interface UILabel (FitToHeight)

@property (nonatomic, assign) BOOL adjustsFontSizeToFitHeight;

@end

UILabel+FitToHeight.m

#import "UILabel+FitToHeight.h"

#import <objc/runtime.h>

@implementation UILabel (FitToHeight)

-(BOOL)adjustsFontSizeToFitHeight {
    NSNumber *number = objc_getAssociatedObject(self, @selector(adjustsFontSizeToFitHeight));
    return [number boolValue];
}

-(void)setAdjustsFontSizeToFitHeight:(BOOL)adjustsFontSizeToFitHeight {
    NSNumber *number = [NSNumber numberWithBool:adjustsFontSizeToFitHeight];
    objc_setAssociatedObject(self, @selector(adjustsFontSizeToFitHeight), number, OBJC_ASSOCIATION_ASSIGN);
}

-(UIFont *)fontToFitHeight {
    float desiredHeight = [self frame].size.height;
    float guess;
    float guessHeight;

    guess = [[self font] pointSize];
    guessHeight = [self sizeIf:guess];
    if(guessHeight == desiredHeight) {
        return [[self font] fontWithSize:guess];
    }

    int attempts = 4;
    while(attempts > 0) {
        guess = guess * (desiredHeight / guessHeight);
        guessHeight = [self sizeIf:guess];

        if(guessHeight == desiredHeight) {
            return [[self font] fontWithSize:guess];
        }

        attempts--;
    }

    return [[self font] fontWithSize:guess];
}

-(float)sizeIf:(float)sizeToTry {
    CGSize size = [[self text] sizeWithAttributes:@{ NSFontAttributeName : [[self font] fontWithSize:sizeToTry] }];
    return size.height;
}

-(void)layoutSubviews {
    [super layoutSubviews];

    if([self adjustsFontSizeToFitHeight]) {
        [self setFont:[self fontToFitHeight]];
    }
}

Import as you would any other category...

#import "UILabel+FitToHeight.h"

and use as follows...

UILabel *titleLabel = [[UILabel alloc] init];
[titleLabel setAdjustsFontSizeToFitHeight:YES];
[titleLabel setAdjustsFontSizeToFitWidth:YES];

It's worth noting that this still works with [titleLabel setAdjustsFontSizeToFitWidth:YES]; so the using the two in conjunction is entirely possible.

1
votes

SWIFT variation:

I managed to do it with an extension. Works fine, min font size is 5. I subtract 10 from the height, so I leave a "margin" also, but you can delete it or modify it.

extension UILabel {

//Finds and sets a font size that matches the height of the frame. 
//Use in case the font size is epic huge and you need to resize it.
func resizeToFitHeight(){
    var currentfontSize = font.pointSize
    let minFontsize = CGFloat(5)
    let constrainedSize = CGSizeMake(frame.width, CGFloat.max)

    while (currentfontSize >= minFontsize){
        let newFont = font.fontWithSize(currentfontSize)
        let attributedText: NSAttributedString = NSAttributedString(string: text!, attributes: [NSFontAttributeName: newFont])
        let rect: CGRect = attributedText.boundingRectWithSize(constrainedSize, options: .UsesLineFragmentOrigin, context: nil)
        let size: CGSize = rect.size

        if (size.height < frame.height - 10) {
            font = newFont
            break;
        }

        currentfontSize--
    }

    //In case the text is too long, we still show something... ;)
    if (currentfontSize == minFontsize){
        font = font.fontWithSize(currentfontSize)
    }
}

}

1
votes

Building off of Joel Fisher's epic answer but written as a Swift 4 extension:

extension String {

    /// Attempts to return the font specified by name of the appropriate point
    /// size for this string to fit within a particular container size and
    /// constrained to a lower and upper bound point size.
    /// - parameter name: of the font.
    /// - parameter containerSize: that this string should fit inside.
    /// - parameter lowerBound: minimum allowable point size of this font.
    /// - parameter upperBound: maximum allowable point size of this font.
    /// - returns: the font specified by name of the appropriate point
    /// size for this string to fit within a particular container size and
    /// constrained to a lower and upper bound point size; `nil` if no such
    /// font exists.
    public func font(named name: String,
                     toFit containerSize: CGSize,
                     noSmallerThan lowerBound: CGFloat = 1.0,
                     noLargerThan upperBound: CGFloat = 256.0) -> UIFont? {
        let lowerBound = lowerBound > upperBound ? upperBound : lowerBound
        let mid = lowerBound + (upperBound - lowerBound) / 2
        guard let tempFont = UIFont(name: name, size: mid) else { return nil }
        let difference = containerSize.height -
            self.size(withAttributes:
                [NSAttributedStringKey.font : tempFont]).height
        if mid == lowerBound || mid == upperBound {
            return UIFont(name: name, size: difference < 0 ? mid - 1 : mid)
        }
        return difference < 0 ? font(named: name,
                                     toFit: containerSize,
                                     noSmallerThan: mid,
                                     noLargerThan: mid - 1) :
            (difference > 0 ? font(named: name,
                                   toFit: containerSize,
                                   noSmallerThan: mid,
                                   noLargerThan: mid - 1) :
                UIFont(name: name, size: mid))
    }

    /// Returns the system font of the appropriate point size for this string
    /// to fit within a particular container size and constrained to a lower
    /// and upper bound point size.
    /// - parameter containerSize: that this string should fit inside.
    /// - parameter lowerBound: minimum allowable point size of this font.
    /// - parameter upperBound: maximum allowable point size of this font.
    /// - returns: the system font of the appropriate point size for this string
    /// to fit within a particular container size and constrained to a lower
    /// and upper bound point size.
    public func systemFont(toFit containerSize: CGSize,
                           noSmallerThan lowerBound: CGFloat = 1.0,
                           noLargerThan upperBound: CGFloat = 256.0) -> UIFont {
        let lowerBound = lowerBound > upperBound ? upperBound : lowerBound
        let mid = lowerBound + (upperBound - lowerBound) / 2
        let tempFont = UIFont.systemFont(ofSize: mid)
        let difference = containerSize.height -
            self.size(withAttributes:
                [NSAttributedStringKey.font : tempFont]).height
        if mid == lowerBound || mid == upperBound {
            return UIFont.systemFont(ofSize: difference < 0 ? mid - 1 : mid)
        }
        return difference < 0 ? systemFont(toFit: containerSize,
                                           noSmallerThan: mid,
                                           noLargerThan: mid - 1) :
            (difference > 0 ? systemFont(toFit: containerSize,
                                         noSmallerThan: mid,
                                         noLargerThan: mid - 1) :
                UIFont.systemFont(ofSize: mid))
    }

}

Usage:

let font = "Test string".font(named: "Courier New",
                              toFit: CGSize(width: 150.0, height: 30.0),
                              noSmallerThan: 12.0,
                              noLargerThan: 20.0)
let sysfont = "Test string".systemFont(toFit: CGSize(width: 150.0, height: 30.0),
                                       noSmallerThan: 12.0,
                                       noLargerThan: 20.0)
0
votes

For UILabels that resize proportionally for larger/smaller devices:

Most effective solution for me has been to set the font's point-size to some ratio of the label's height +/- an adjustment factor. Assuming use of auto-layout constraints, position it's y vertical-center aligned to the bottom of the superview, multiplied by a ratio. Similarly in IB, constrain label's width to a proportion of screen's width.

Optionally, you may lock in the label's height/width ratio with an aspect constraint, however this may cause clipping if you don't get the font's point-size calculation right. The only reason to lock aspect ratio is if other controls/views' positions are relative to this label. However I highly recommend placing such controls/views relative to the superview's height/width so that they are not dependent on this label.

I understand this isn't exactly an encapsulated solution, but it has consistently caused me the least amount of grief. The only other solution that came close made use of while loops, however in my case I couldn't deal with the delays they imposed for upon every layout/refresh system call.

0
votes

My apologies, if I have missed something here in all the text.

I followed @Crazyrems suggestions for autoshrinking the label's font. This does scale the font based on width as others have observed.

Then I just set 'Lines' to 0 in the UILabel's font section of Xcode. In code, that should be numberOfLines. That's all.

Credit goes to @Mikrasya, who hinted on this solution in one of the comments above.

Tested on Xcode 7.3 and iOS 9.3.2.

0
votes

Forgive me if I am wrong but everything mentioned here is unnecessary. Set your font again just after the change with a new fontSize of yourLabel.height

You can also check for a conditional comparison between these values (yourLabel.height and fontSize) to prevent unnecessary updates.

All you need to do is:

[yourLabel setFont:[UIFont fontWithName:@"*your fontname*" size:yourLabel.frame.size.height]];
0
votes

I made a macro to do this for you

///Scales FontSize up (or down) until the text fits within the height of the label, will not auto-update, must be called any time text is updated. Label Frame must be set prior to calling
#define scaleFontSizeToFillHeight(__label) {\
__label.font = [UIFont fontWithName:__label.font.fontName size:__label.frame.size.height*2.0f];\
UIFont *__currentFont = __label.font;\
CGFloat __originalFontSize = __currentFont.pointSize;\
CGSize __currentSize = [__label.text sizeWithAttributes:@{NSFontAttributeName : __currentFont}];\
while (__currentSize.height > __label.frame.size.height && __currentFont.pointSize > (__originalFontSize * __label.minimumScaleFactor)) {\
__currentFont = [__currentFont fontWithSize:__currentFont.pointSize - 1];\
__currentSize = [__label.text sizeWithAttributes:@{NSFontAttributeName : __currentFont}];\
}\
__label.font = __currentFont;\
}
0
votes

The accepted answer has a bug in it. The variable distance must be a float, or it can return a font size that is too big. Also, the use of "- (CGSize)sizeWithFont:(UIFont *)font;" is deprecated. Here's the code with these 2 issues fixed.

+ (UIFont *)findAdaptiveFontWithName:(NSString *)fontName forUILabelSize:(float)maxHeight withMaxFontSize:(int)maxFontSize
{
    UIFont *tempFont = nil;
    NSString *testString = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    NSInteger tempMin = 0;
    NSInteger tempMax = maxFontSize;
    NSInteger mid = 0;
    float difference = 0;

    while (tempMin <= tempMax) {
        mid = tempMin + (tempMax - tempMin) / 2;
        tempFont = [UIFont fontWithName:fontName size:mid];

        UILabel* dummyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        dummyLabel.text = testString;
        dummyLabel.font = tempFont;
        [dummyLabel sizeToFit];

        difference = maxHeight - dummyLabel.bounds.size.height;

        if (mid == tempMin || mid == tempMax) {
            if (difference < 0) {
                return [UIFont fontWithName:fontName size:(mid - 1)];
            }

            return [UIFont fontWithName:fontName size:mid];
        }

        if (difference < 0) {
            tempMax = mid - 1;
        } else if (difference > 0) {
            tempMin = mid + 1;
        } else {
            return [UIFont fontWithName:fontName size:mid];
        }
    }

    return [UIFont fontWithName:fontName size:mid];
}
0
votes

This seemed to work for me, I've subclassed UILabel and in the layoutSubviews i've checked for the actual height and adjusted the font size accordingly.

import UIKit

class HeightAdjustableLabel: UILabel {

    override func layoutSubviews() {
        super.layoutSubviews()
        if frame.height < font.pointSize + 2 {
            font = font.withSize(frame.height - 2)
        }
    }
}
-7
votes

Yeah, go to interface builder, (your .xib file) and go to the third tab from the right in the attributes inspector and you may set the size of the font there!