28
votes

I have two UILabels next to each other in row with left and right adjustments so that it looks like below.

 |-Some text left adjusted----------some other text right adjusted-|

Both labels have adjustsFontSizeToFitWidth = YES and are linked to each other with the following constraint

[NSLayoutConstraint constraintWithItem:_rightLabel
                    attribute:NSLayoutAttributeLeft
                    relatedBy:NSLayoutRelationGreaterThanOrEqual
                    toItem:_leftLabel
                    attribute:NSLayoutAttributeRight
                    multiplier:1
                    constant:10]

So that they take up as much space as they can and if there is not enough space for the original font size it will be lowered thanks to adjustsFontSizeToFitWidth so that no text is truncated.

My problem is that when one needs to lower its font size due to long text i want the other label to lower its font size as well so that both are the same size instead of one being perhaps twice the size of the other. I would like to constraint the font size as well to match but alas i do not know how to this, any ideas?

4
I managed to get something like the desired effect by imposing an equal widths constraint between both labels..(the font scaling is the same in both though the last two characters are truncated with ..)Aodh

4 Answers

15
votes

From the UILabel documentation on adjustsFontSizeToWidth:

Normally, the label text is drawn with the font you specify in the font property. If this property is set to YES, however, and the text in the text property exceeds the label’s bounding rectangle, the receiver starts reducing the font size until the string fits or the minimum font size is reached.

I infer from this that the updated font is calculated at drawing time, and the font property is only read, not written to. Therefore, I believe the suggestion by Andrew to use KVO on the font property will not work.

Consequently, to achieve the result you want, you'll need to calculate the adjusted font size.

As Jackson notes in the comments, this very convenient NSString method to get the actual font has been deprecated in iOS 7. Technically, you could still use it until it's removed.

Another alternative is to loop through font scales until you find one that will fit both labels. I was able to get it working fine; here's a sample project that shows how I did it.

Also, here's the code in case that link ever stops working:

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:_rightLabel
                                                                  attribute:NSLayoutAttributeLeft
                                                                  relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                     toItem:_leftLabel
                                                                  attribute:NSLayoutAttributeRight
                                                                 multiplier:1
                                                                   constant:10];

    [self.view addConstraint:constraint];
}

- (IBAction)makeRightLabelLongerPressed:(id)sender {
    self.rightLabel.text = @"some much longer right label text";
}

- (IBAction)adjustLabelSizes:(id)sender {
    NSLog(@"Attempting to adjust label sizes…");

    CGFloat minimumScaleFactor = fmaxf(self.rightLabel.minimumScaleFactor, self.leftLabel.minimumScaleFactor);;
    UIFont * startingFont = self.rightLabel.font;

    for (double currentScaleFactor = 1.0; currentScaleFactor > minimumScaleFactor; currentScaleFactor -= 0.05) {
        UIFont *font = [startingFont fontWithSize:startingFont.pointSize * currentScaleFactor];
        NSLog(@"  Attempting font with scale %f (size = %f)…", currentScaleFactor, font.pointSize);

        BOOL leftLabelWorks = [self wouldThisFont:font workForThisLabel:self.leftLabel];
        BOOL rightLabelWorks = [self wouldThisFont:font workForThisLabel:self.rightLabel];
        if (leftLabelWorks && rightLabelWorks) {
            NSLog(@"    It fits!");
            self.leftLabel.font = font;
            self.rightLabel.font = font;
            return;
        } else {
            NSLog(@"    It didn't fit. :-(");
        }

    }

    NSLog(@"  It won't fit without violating the minimum scale (%f), so set both to minimum.  Some text may get truncated.", minimumScaleFactor);

    UIFont *minimumFont = [self.rightLabel.font fontWithSize:self.rightLabel.font.pointSize * self.rightLabel.minimumScaleFactor];
    self.rightLabel.font = minimumFont;
    self.leftLabel.font = minimumFont;
}

- (BOOL) wouldThisFont:(UIFont *)testFont workForThisLabel:(UILabel *)testLabel {
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:testFont, NSFontAttributeName, nil];
    NSAttributedString *as = [[NSAttributedString alloc] initWithString:testLabel.text attributes:attributes];
    CGRect bounds = [as boundingRectWithSize:CGSizeMake(CGRectGetWidth(testLabel.frame), CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin) context:nil];
    BOOL itWorks = [self doesThisSize:bounds.size fitInThisSize:testLabel.bounds.size];
    return itWorks;
}

- (BOOL)doesThisSize:(CGSize)aa fitInThisSize:(CGSize)bb {
    if ( aa.width > bb.width ) return NO;
    if ( aa.height > bb.height ) return NO;
    return YES;
}

This approach could be trivially refactored into a category method that replaces the deprecated method linked to by Jackson.

2
votes

You can solve this problem this way:

Swift 5

extension UILabel {
    var actualFontSize: CGFloat {
        guard let attributedText = attributedText else { return font.pointSize }
        let text = NSMutableAttributedString(attributedString: attributedText)
        text.setAttributes([.font: font as Any], range: NSRange(location: 0, length: text.length))
        let context = NSStringDrawingContext()
        context.minimumScaleFactor = minimumScaleFactor
        text.boundingRect(with: frame.size, options: .usesLineFragmentOrigin, context: context)
        let adjustedFontSize: CGFloat = font.pointSize * context.actualScaleFactor
        return adjustedFontSize
    } 
}

Usage:

firstLabel.text = firstText
secondLabel.text = secondText
view.setNeedsLayout()
view.layoutIfNeeded()
let smallestSize = min(firstLabel.actualFontSize, secondLabel.actualFontSize)
firstLabel.font = firstLabel.font.withSize(smallestSize)
secondLabel.font = secondLabel.font.withSize(smallestSize)
-1
votes

You could try using key-value observing to observe changes to the font property on one label and when it does, set the other label to use the same font.

In your -viewDidLoad method:

// Add self as an observer of _rightLabel's font property
[_rightLabel addObserver:self forKeyPath:@"font" options:NSKeyValueObservingOptionNew context:NULL];

In the same controller implementation (self in the above code snippets context):

// Observe changes to the font property of _rightLabel
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == _rightLabel && [keyPath isEqualToString:@"font"]) {
        // Set _leftLabel's font property to be the new value set on _rightLabel
        _leftLabel.font = change[NSKeyValueChangeNewKey];
    }
}
-2
votes

I found a better solution. Just add to ur text spaces from begin to end to uilabel, which have less text length. And font will same.