How to set max height and fixed width for UITextView and force ellipsis rather than scroll if overflow occurs?

StackOverflow https://stackoverflow.com/questions/22594889

Question

Here's my goal:

  1. I want to use a UITextView rather than a UILabel because I want users to be able to select text and copy.
  2. I want the UITextView to max out at a height of 60 points.
  3. I want the UITextView to have a fixed width of 300 points.
  4. I want the UITextView to line break on words.
  5. Let's say, based on the attributed text string I feed it, that it takes 3 lines to reach the 60 point max height. Therefore, if I feed the UITextView 6 lines worth of attributed text I want the UITextView to max out at 60 points and display 3 lines followed by an ellipsis (e.g. ...).
  6. I don't want the text view to ever be scrollable.
  7. If I feed the UITextView a single word as attributed text, such as "Hello", I want the UITextView to still have a fixed width of 300 points but a dynamic height that scales to as small as it can be, approximately 20 points for a single line of text in this example.
  8. I want the UITextView to have zero internal padding.

Any ideas?

Was it helpful?

Solution

- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width
{
    UITextView *calculationView = [[UITextView alloc] init];
    [calculationView setAttributedText:text];
    CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
    return size.height;
}

- (void)viewDidLoad
{
    // Invoke super
    [super viewDidLoad];

    // Get text of unknown length
    NSMutableAttributedString *myAttributedString = [[NSMutableAttributedString alloc] initWithString:@"String of unknown length here..." attributes:@{NSForegroundColorAttributeName : [UIColor redColor]}];

    // Get ellipsis w/ matching attributes
    NSDictionary *endCharAttributes = [myAttributedString attributesAtIndex:myAttributedString.length - 1 effectiveRange:NULL];
    NSAttributedString *ellipsis = [[NSAttributedString alloc] initWithString:@"..." attributes:endCharAttributes];

    // Define size constraints
    CGFloat maxHeight = 60;
    CGFloat fixedWidth = 300;

    // Get starting height
    CGFloat textViewHeight = [self textViewHeightForAttributedText:myAttributedString andWidth:fixedWidth];

    // Reduce string size and add ellipsis until we fit within our height constraint
    while (textViewHeight > maxHeight)
    {
        NSLog(@"%f", textViewHeight);
        NSRange substringRange = {0, myAttributedString.length - 6}; // Reducing by 6 works for my app (strings are never huge)
        myAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[myAttributedString attributedSubstringFromRange:substringRange]];
        [myAttributedString appendAttributedString:ellipsis];
        NSLog(@"myAttributedString = %@", myAttributedString);
        textViewHeight = [self textViewHeightForAttributedText:myAttributedString andWidth:fixedWidth];
    }

    // Init and config UITextView
    UITextView *textView = [[UITextView alloc] init];
    textView.attributedText = myAttributedString;
    textView.frame = CGRectMake(0, 0, fixedWidth, textViewHeight);
    [self.view addSubview:textView];
}

Have a more elegant solution? Post it!

UPDATE: You can increase the performance of - (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width by adding a helpers class and implementing the the following class methods:

// Private, gets us to alloc init calculation view one time for life of application
+ (UITextView *)calculationView
{
    static UITextView *_calculationView;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _calculationView = [[UITextView alloc] init];
    });
    return _calculationView;
}

// Public, app calls this a lot
+ (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width usingUIEdgeInset:(UIEdgeInsets)edgeInsets
{
    [self calculationView].textContainerInset = edgeInsets;
    [self calculationView].attributedText = text;
    CGSize size = [[self calculationView] sizeThatFits:CGSizeMake(width, FLT_MAX)];
    return size.height;
}

OTHER TIPS

Have you considered subclassing UILabel and adding the capability to select and copy text? Mattt Thompson has a good example here.

My solution is

- (NSString*)stringByTruncatingToWidth:(CGFloat)width maxHeight:(CGFloat)maxHeight font:(UIFont *)font;
    {
        NSString *ellipsis = @"…";
        NSMutableString *truncatedString = [self mutableCopy];
        if ([self textSizeForMaxWidth:width font:font].height > maxHeight)
        {
            truncatedString = [[truncatedString stringByAppendingString:ellipsis] mutableCopy];
            NSRange range = {truncatedString.length - 4, 1};
            [truncatedString deleteCharactersInRange:range];
            while ([truncatedString textSizeForMaxWidth:width font:font].height > maxHeight){
                [truncatedString deleteCharactersInRange:range];
                range.location--;
            }
        }
        return truncatedString;
    }

And help method for calculating text size for max width

- (CGSize)textSizeForMaxWidth:(CGFloat)width font:(UIFont *)font
    {
        NSTextStorage *textStorage = [[NSTextStorage alloc]
                                      initWithString:self];
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize: CGSizeMake(width, MAXFLOAT)];
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
        [layoutManager addTextContainer:textContainer];
        [textStorage addLayoutManager:layoutManager];
        [textStorage addAttribute:NSFontAttributeName value:font
                            range:NSMakeRange(0, [textStorage length])];
        [textContainer setLineFragmentPadding:0.0];

        [layoutManager glyphRangeForTextContainer:textContainer];
        CGRect frame = [layoutManager usedRectForTextContainer:textContainer];
        return frame.size;
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top