1

I'm implementing an autogrowing UITextView. I'm aiming for a similar behaviour of the message box in Whatsapp, which autogrows when your text has more than 1 line.

I'm using the approach described below which stores the height constraint in a UITextView subclass and modifies it when the text changes.

My solution animates correctly when I press the enter key inside the TextView, but it doesn't work when my typing goes over the end of the line. In this case it just changes size instantly.

Performing the animation on the delegate's - (void)textViewDidChange:(UITextView *)textView method produces the same result.

How can I correctly animate the TextView height using the auto layout system?

I'm implementing it like this:

@interface OEAutoGrowingTextView ()

@property (strong, nonatomic) NSLayoutConstraint *heightConstraint;

@end


@implementation OEAutoGrowingTextView

- (id)initWithFrame:(CGRect)frame
{
    if ( !(self = [super initWithFrame:frame]) )
    {
        return nil;
    }

    [self commonInit];

    return self;
}

- (void)awakeFromNib
{
    [self commonInit];
}

- (void)commonInit
{
    // If we are using auto layouts, than get a handler to the height constraint.
    for (NSLayoutConstraint *constraint in self.constraints)
    {
        if (constraint.firstAttribute == NSLayoutAttributeHeight)
        {
            self.heightConstraint = constraint;
            break;
        }
    }

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:UITextViewTextDidChangeNotification object:self];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)textDidChange:(NSNotification *)notification
{
    self.heightConstraint.constant = self.contentSize.height;
    [UIView animateWithDuration:1.0f animations:^
     {
         [self layoutIfNeeded];
     }];
}

@end

Note: doing the following doesn't help.

- (void)textDidChange:(NSNotification *)notification
{
    self.heightConstraint.constant = self.contentSize.height;
    [UIView animateWithDuration:1.0f animations:^
     {
         [self layoutIfNeeded];
         for (UIView *view in self.subviews)
         {
             [view layoutIfNeeded];
         }
     }];
}

Further update: This seems to be a bug in iOS 7.x, I think it's fixed on iOS 8.0.

Ricardo Sanchez-Saez
  • 9,466
  • 8
  • 53
  • 92

2 Answers2

4

I tried wrapping the heightConstraint change in a UIView animation block and that didn't work

That isn't how you animate a constraint change. You do it by changing the constraint and then animating the act of layout itself, like this:

// change the text view constraint here
[UIView animateWithDuration:duration animations:^{
    [self.textView layoutIfNeeded];
}];
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Yes, but how can one trigger the animation given that the constraint change happens automatically in the autoLayout method of the UITextView? – Ricardo Sanchez-Saez May 16 '14 at 17:36
  • It doesn't happen automatically. If your text view is changing size, that is because you are changing its size. So if you can change its constraint directly, you can change its constraint with animation. – matt May 16 '14 at 17:38
  • It doesn't seem good practice to animate `layoutIfNeeded` inside the `layoutSubviews`; and it also doesn't work as expected. What I'm trying to ask is, where should I animate `layoutIfNeeded`, given that the heightConstraint change happens in `layoutSubviews` of the `UITextView`? – Ricardo Sanchez-Saez May 16 '14 at 17:49
  • It isn't a given. You don't have to copy that one example slavishly. – matt May 16 '14 at 17:50
  • It seemed like a good idea to have the UITextView automatically handle its own size based on content. What's your suggested approach? – Ricardo Sanchez-Saez May 16 '14 at 17:51
  • 1
    My downloadable example is here: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch10p531selfSizingTextView/ch23p811selfSizingTextField/ViewController.m No reason you couldn't add animation to that example. – matt May 16 '14 at 18:05
  • And, can't this by done internally in a UITextView subclass, instead of doing it on the delegate? – Ricardo Sanchez-Saez Jun 09 '14 at 18:33
  • I have an additional problem with your solution (animating `[self.textView layoutIfNeeded]` in the delegate) . When I press *Enter* inside the TextView, it animates as it should. When I continue to type a word reaching the end of the textView, it doesn't animate. – Ricardo Sanchez-Saez Jun 09 '14 at 21:03
  • 1
    @RicardoSánchez-Sáez It wouldn't surprise me if this involved UITextView bugs in iOS 7, as there are loads of them...! – matt Jun 10 '14 at 01:52
1

Ok, the issue is that as of ios7, .contentSize isn't correct for UITextViews. I have this functionality, and you need to compute the contentSize yourself. I added a category method to UITextView, -contentHeight, and use that instead to compute the contentSize.

See these two links.

UITextView Content Size

SO on the same question

Here is the code that fixes it:

@implementation UITextView (Sizing)

- (CGFloat)contentHeight {
    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) {
        // This is the code for iOS 7. contentSize no longer returns the correct value, so
        // we have to calculate it.
        //
        // This is partly borrowed from HPGrowingTextView, but I've replaced the
        // magic fudge factors with the calculated values (having worked out where
        // they came from)

        CGRect frame = self.bounds;

        // Take account of the padding added around the text.

        UIEdgeInsets textContainerInsets = self.textContainerInset;
        UIEdgeInsets contentInsets = self.contentInset;

        CGFloat leftRightPadding = textContainerInsets.left + textContainerInsets.right + self.textContainer.lineFragmentPadding * 2;
        leftRightPadding += contentInsets.left + contentInsets.right;

        CGFloat topBottomPadding = textContainerInsets.top + textContainerInsets.bottom + contentInsets.top + contentInsets.bottom;

        frame.size.width -= leftRightPadding;
        frame.size.height -= topBottomPadding;

        NSString* textToMeasure = self.text;
        if(![textToMeasure isNotEmpty])
            textToMeasure = @"-";

        if ([textToMeasure hasSuffix:@"\n"]) {
            textToMeasure = [NSString stringWithFormat:@"%@-", self.text];
        }

        // NSString class method: boundingRectWithSize:options:attributes:context is
        // available only on ios7.0 sdk.

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

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

        CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
                                                  options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                               attributes:attributes
                                                  context:nil];

        CGFloat measuredHeight = ceilf(CGRectGetHeight(size) + topBottomPadding);
        return measuredHeight;
    } else {
        return self.contentSize.height;
    }
}

@end

Instead of contentSize, use this to compute the content height. You also don't need the animate at all - mine just computes and that is smooth enough, so you should make sure you really need the animation.

Community
  • 1
  • 1
rvijay007
  • 1,357
  • 12
  • 20