Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AutoLayout link two UILabels to have the same font size

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?

like image 359
Alzorz Avatar asked Nov 28 '13 09:11

Alzorz


2 Answers

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.

like image 183
Aaron Brager Avatar answered Nov 07 '22 01:11

Aaron Brager


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)
like image 26
Nikaaner Avatar answered Nov 07 '22 01:11

Nikaaner