Search code examples
iosiphoneios7uitextviewnsattributedstring

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


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?


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;
    }