Search code examples
iosobjective-cnsstringuilabelsizetofit

Positioning a UILabel directly beneath another UILabel


I have an addition to NSString which automatically resizes a UILabel depending on the text that's being read into it (I have a simple app showing quotations, so some are a few words, some a couple sentences). Below that quote label, I also have an author label, which (oddly enough) has the author of the quote in it.

I'm trying to position that author label directly beneath the quote label (as in, its y coordinate would be the quote label's y coordinate plus the quote label's height. What I'm seeing is some space being placed between the two labels, that depending on the length of the quote, changes size. Smaller quotes have more space, while longer quotes have less space. Here's a quick diagram of what I'm seeing:

enter image description here enter image description here

Note the gap between the red and blue boxes (which I've set up using layer.borderColor/borderWidth so I can see them in the app), is larger the shorter the quote is.

If anyone can sift through the code below and help point me towards exactly what's causing the discrepancy, I'd be really grateful. From what I can see, the author label should always be 35 pixels beneath the quote label's y + height value.

Just to confirm: everything is hooked up correctly in Interface Builder, etc. The content of the quote's getting in there fine, everything else works, so it's hooked up, that isn't the issue.

To clarify, my question is: Why is the gap between the labels changing dependant on the quote's length, and how can I get a stable, settable gap of 35 pixels correctly?

Here's the code I'm using to position the labels:

// Fill and format Quote Details
_quoteLabel.text = [NSString stringWithFormat:@"\"%@\"", _selectedQuote.quote];
_authorLabel.text = _selectedQuote.author;
[_quoteLabel setFont: [UIFont fontWithName: kScriptFont size: 28.0f]];
[_authorLabel setFont: [UIFont fontWithName: kScriptFontAuthor size: 30.0f]];

// Automatically resize the label, then center it again.
[_quoteLabel sizeToFitMultipleLines];
[_quoteLabel setFrame: CGRectMake(11, 11, 298, _quoteLabel.frame.size.height)];

// Position the author label below the quote label, however high it is.
[_authorLabel setFrame: CGRectMake(11, 11 + _quoteLabel.frame.size.height + 35, _authorLabel.frame.size.width, _authorLabel.frame.size.height)];

Here's my custom method for sizeToFitMultipleLines:

- (void) sizeToFitMultipleLines
{
    if (self.adjustsFontSizeToFitWidth) {
        CGFloat adjustedFontSize = [self.text fontSizeWithFont: self.font constrainedToSize: self.frame.size minimumFontSize: self.minimumScaleFactor];
        self.font = [self.font fontWithSize: adjustedFontSize];
    }
    [self sizeToFit];
}

And here's my fontSizeWithFont:constrainedToSize:minimumFontSize: method:

- (CGFloat) fontSizeWithFont: (UIFont *) font constrainedToSize: (CGSize) size minimumFontSize: (CGFloat) minimumFontSize
{
    CGFloat fontSize = [font pointSize];
    CGFloat height = [self sizeWithFont: font constrainedToSize: CGSizeMake(size.width, FLT_MAX) lineBreakMode: NSLineBreakByWordWrapping].height;
    UIFont *newFont = font;

    // Reduce font size while too large, break if no height (empty string)
    while (height > size.height && height != 0 && fontSize > minimumFontSize) {
        fontSize--;
        newFont = [UIFont fontWithName: font.fontName size: fontSize];
        height = [self sizeWithFont: newFont constrainedToSize: CGSizeMake(size.width, FLT_MAX) lineBreakMode: NSLineBreakByWordWrapping].height;
    };

    // Loop through words in string and resize to fit
    for (NSString *word in [self componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
        CGFloat width = [word sizeWithFont: newFont].width;
        while (width > size.width && width != 0 && fontSize > minimumFontSize) {
            fontSize--;
            newFont = [UIFont fontWithName: font.fontName size: fontSize];
            width = [word sizeWithFont: newFont].width;
        }
    }
    return fontSize;
}

Solution

  • After you called size to fit on both labels, calculate the distance between their frames and change them accordingly:

    [quoteLabel sizeToFit];
    [authorLabel sizeToFit];
    
    float distance = authorLabel.frame.origin.y - quoteLabel.frame.size.height;
    float difference = distance - 35;
    
    authorLabel.frame = CGRectMake(authorLabel.frame.origin.x,(authorLabel.frame.origin.y - difference),authorLabel.frame.size.width,authorLabel.frame.size.height);
    

    The reason the gap changes is that the quote label frame changes its height dependent on its content when you call sizeToFit.

    UPDATE

    Given the recent developments in the comments, I think you have 3 possibilities:

    • resize the whitespace instead of only the words, so that the string actually fits in the frame correctly
    • somehow access the CTFramesetter of UILabel to see what the actual frame, when all is said and done, amounts to
    • make your own UIView subclass that handles Core Text drawing in its draw rect method (should be easy in your case), since after all you are trying to give to UILabel a behavior that it's not meant for