Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

boundingRectWithSize not replicating UITextView

My requirement in a project is that the font size of the UITextView should decrease according the content of the UITextView. So i am trying to do estimate the size of the text using boundingRectWithSize.

The problem is that the font size I get is a bit too big and some part of the text does get clipped.

My Function :

 -(BOOL)updateTextViewFontSizeForText:(NSString*)text{

    float fontSize = self.maximumFontSizeInPoints;

    self.font = [self.font fontWithSize:fontSize];

    CGSize tallerSize ;
    CGSize stringSize ;


    do
    {
        if (fontSize <= self.minimumFontSizeInPoints) // it just won't fit
            return NO;

        fontSize -= 1.0;
        self.font = [self.font fontWithSize:fontSize];

        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];

        NSDictionary *attributes = @{ NSFontAttributeName: self.font, NSParagraphStyleAttributeName : paragraphStyle };



        tallerSize = CGSizeMake(self.frame.size.width,self.frame.size.height-16);// the 16 is given because uitextview adds some offset
        stringSize = [text boundingRectWithSize:CGSizeMake(self.contentSize.width,CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:attributes context:nil].size;
    }while(stringSize.height >= tallerSize.height);


    if ([self.onTextChangDelegate respondsToSelector:@selector(onTextChangDelegate)]) {

        [self.onTextChangDelegate onTextChanged:text];
    }

    return YES;
}
like image 675
Tapan Thaker Avatar asked Apr 03 '14 10:04

Tapan Thaker


1 Answers

I ran into the same issue when trying to do the same thing.

The issue is how UITextView run's its line-breaks compared to boundingRectWithSize. You can read more details here: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Concepts/CalcTextLayout.html

But you can actually calculate the exact size! There are basically two properties of a UITextView that you'll need to take into account in order to get correct size estimates. The first is textContainer.lineFragmentPadding, the second is textContainerInset.

First, textContainer.lineFragmentPadding: You may have noticed that your sizing is generally always off by 10px, this is because the systems default value is 5px. When you're calculating your estimated size, you'll need to subtract this value from the size you're checking against and add it back when you have your final value.

Second, textContainerInset. This is a UIEdgeInset that you'll need to add back to your final calculated value to match the systems.

This is code based on how I solved the issue:

- (CGSize)sizeThatFits:(CGSize)size
    CGFloat lineFragmentPaddings = self.textContainer.lineFragmentPadding * 2;
    CGFloat horzPadding = self.textContainerInset.left + self.textContainerInset.right + lineFragmentPaddings;
    CGFloat vertPadding = self.textContainerInset.top + self.textContainerInset.bottom;

    size.width -= horzPadding;
    CGRect boundingRect = [attributedText boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin context:nil];
    size = boundingRect.size;
    // I found through debugging that adding 0.25 rounded
    // matches sizeThatFits: identically. Not sure why…
    size.width += horzPadding + 0.25;
    size.height += vertPadding + 0.25;
    size = CGSizeRound(size);

    return size;
}

Note, CGSizeRound is just a custom function I wrote that rounds the width and height of the CGSize to the nearest 0.5.

For comparison, if you create a second UITextView, and make sure the textContainer.lineFragmentPadding and textContainerInset are the same, you should see the values almost identical to the nearest 0.5.

And to your question about calculating a proper pointSize, this is some pseudo code for that:

CGFloat pointSize = 64;
CGFloat minPointSize = 32;
CGFloat decrementor = 4;
CGFloat padding = self.textContainerInset.left + self.textContainerInset.right + lineFragmentPaddings;
CGFloat actualWidth = self.maxTextViewSize.width - padding * 2;
CGRect boundingRect = CGRectZero;
BOOL isValidPointSize = NO;
do {
    if (pointSize < minPointSize) {
        pointSize = minPointSize;
        boundingRect.size.height = self.maxTextViewSize.height;
        isValidPointSize = YES;
    } else {
        NSDictionary *defaultAttributes = [self.customTextStorage defaultAttributesForPointSize:pointSize];
        NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:defaultAttributes];

        boundingRect = [attrString boundingRectWithSize:CGSizeMake(actualWidth, 1024) options:NSStringDrawingUsesLineFragmentOrigin context:nil];
        // is the height to big?
        if (boundingRect.size.height > self.maxTextViewSize.height) {
            // reduce the point size for next iteration of loop
            pointSize -= decrementor;
        }
        // passes height test
        else {
            isValidPointSize = YES;
        }
    }
} while (!isValidPointSize);

return pointSize;

Again, the above is pseudo code based on my implementation (not meant for just drop in replacement for what you have). Hope this helps!

like image 191
Andy Poes Avatar answered Oct 02 '22 01:10

Andy Poes