Celebrating 10 Years!

profile picture

Magical Growing UITextViews inside UITableViewCells

July 10, 2014 - Roundwall Software

I've seen quite a few attempts at solving this problem and all of them seem to be more complicated than necessary.

Many solutions I see involve using a method like this in your tableview's datasource:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [self textForRowAtIndexPath:indexPath];
CGSize newSize = [text
        sizeWithFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]
        constrainedToSize:CGSizeMake(308.0f, CGFLOAT_MAX)
        lineBreakMode:UILineBreakModeWordWrap];

return newSize.height+10.0f;
}

There are a few problems with this method.

  1. Magic numbers: Where does 308.0f come from? Where does 10.0f come from? Why should your tableview's datasource know or care? This puts details about your design into a place that shouldn't give a crap about your designs.
  2. Hard-coded fonts: What happens when you decide to use a different font in your textview? Get ready to bang your head against a wall until you remember to also change the font in your datasource.
  3. The sizeWithFont methods are now deprecated: If you're trying to write Swift, then you can't even use these methods and ignore the warnings. The method they added to replace these methods are broken and will return incorrect sizes all the time.

Confronted with these problems, I poked around for a better way to work. First I made a cell like so:

@protocol RWSGrowingCellDelegate;

@interface RWSGrowingCell : UITableViewCell<UITextViewDelegate>
@property (nonatomic, weak) IBOutlet UITextView *inputField;
@property (nonatomic, weak) id<RWSGrowingCellDelegate> delegate;
@end

@protocol RWSGrowingCellDelegate <NSObject>
- (void)growingCell:(RWSGrowingCell *)cell didChangeSize:(CGSize)size;
@end

This establishes a few things. First, the cell communicates to its delegate (your table's datasource) when the cell's size should change because of the textview. It doesn't say that text changed, it says that size changed. Now design details like a cell's height is inside the cell, not in the controller. This solves problem number 1 and 2. Yay!

That just leaves problem number 3. How do we know how tall that cell should be? Here's what the implementation of the cell looks like:

#import "RWSGrowingCell.h"

@implementation RWSGrowingCell

- (void)awakeFromNib
{
	[super awakeFromNib];

	NSTextContainer *container = self.inputField.textContainer;
	container.widthTracksTextView = YES;
}

- (void)textViewDidChange:(UITextView *)textView
{
	CGFloat height = textView.contentSize.height;
height+= 16.0f;
	[self.delegate growingCell:self didChangeSize:CGSizeMake(self.bounds.size.width, height)];
}

@end

Yep. That's it! First, you tell the text view's storage container that it's width should track with the text view. This tells the container not to try and layout text in a wider space than the text view on screen. We want it to grow vertically, not horizontally. Then you simply need to figure out the appropriate size of the cell by asking the text view for it's content size whenever text changes. No broken text-sizing methods, no deprecated methods, just let the text view do it's job and ask it how big it should be. Then you tack on whatever extra height you need for padding depending on the cell's design. It's ok to do it here because the cell knows about the cell's design.

You can find the full-code version of this solution here.

So there you go. Now go forth and make a ton of apps with growing table cells without as much groaning and face-palming.

**Also keep in mind: since iOS 8 adds support for self-sizing cells with Auto-Layout, even my solution might not be necessary anymore.