AutoLayout link two UILabels to have the same font size
Asked Answered
A

4

32

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?

Antoinette answered 28/11, 2013 at 9:26 Comment(1)
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 ..)Bierman
L
15

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.

Leucocratic answered 29/12, 2013 at 19:39 Comment(4)
I'm not sure I completely understand your scenario. If I'm right about what I think it is, perhaps you could add a third label that contains the concatenated text of the two you've described. The new label would be transparent, its only purpose being to establish the appropriate font size. for the label size. Then the other two labels could simply be justified as appropriate using the same font size as the transparent label.Ricarda
@VictorEngel This code establishes the correct font size without making a third label. Try downloading & running the project.Leucocratic
Sure. I was just suggesting an alternative that used only auto layout. It was intended to be a comment to the original question, not a comment to your answer. Not sure how it ended up here. Sorry about that.Ricarda
Does anyone have a link to the demo source? ThanksCoxa
S
4

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)
Scoggins answered 14/10, 2019 at 11:59 Comment(2)
It's not working with UIFont.prefferedFont type. It's getting super small values like 0.1499....Mercuri
Need to set minimumScaleFactor to make above code workAnnotate
N
-1

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];
    }
}
Niko answered 29/12, 2013 at 4:37 Comment(2)
I don't think the font property ever changes when you use adjustsFontSizeToFitWidth. The font value remains unchanged but when displayed, it's rendered with a smaller font size.Calc
And the only way to calculate the adjusted font size has been deprecated in iOS 7.Calc
K
-3

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.

Kedah answered 8/4, 2015 at 7:25 Comment(2)
This made me laugh - I love the simplicity of it, even if will not work 100% of the time. If you font is not fixed width, a space is not the save width as most other letters. If you have a long string with lots of 'w' (which are very wide) it will still be longer than the short string with the space padding. In other words, even if they are the same amount of characters they are not always the same width. Though in many cases it will be close enough to work.Dotdotage
I can't believe... 👁️👄👁️Mercuri

© 2022 - 2024 — McMap. All rights reserved.