So the project I’m working on now requires text entry. Not a single line of text, for which a UITextField would be well suited, but one or perhaps multiple lines. I’ve been trying to achieve an effect similar to that of the “notes” field in the Apple Contacts app, or the “reminders” field in the Apple Reminders app. These are essentially UITableViewCells that contain a UITextView to allow for text input, and they grow (or shrink) dynamically as the user inputs text.

Dynamic sizing for text entry from Apple's Reminders app.

Another example of self-sizing text entry from Apple's Contacts app.

Sounds simple enough, right? Ha!

It makes me think of a quote I just read from Bertrand Russell, a famous mathematician who suffered permanent burnout from the experience of writing his hallmark Principia Mathematica:

In the end, the work was finished but my intellect never quite recovered from the strain. I have been ever since definitely less capable of dealing with difficult abstractions than I was before. This is part, though no means the whole, of the reason for the change in the nature of my work.

Oh, I’m being melodramatic. It’s not like this was iCloud with Core Data or anything. On to the pork n’ beans!

I’m using iOS 6 with Autolayout and Storyboards for this example. I think half the problem is the learning curve of new technologies, but I’m a firm believer in just adopting everything new as early as possible and just moving forward.

Creating a Self-Sizing TextView and Cell in a Static, Grouped TableView

My specific needs called for a static, grouped UITableView, a la Contacts. This approach is slightly easier because when you create a static tableview using IB and a storyboard, it’s like designing any other view inside a view controller. There’s no need to subclass tableviewcells, you can wire up all your control objects right on the screen in front of you, all your outlets are available when the view loads.

In part 2 of this post, I’ll show you how to achieve the same effect using dynamic table view cells.

I think a lot of times with text entry users want to rotate the device and Auto Layout makes this easier. This approach supports portrait and landscape orientations.

Getting Started

You’ll want to add a Table View Controller object to your storyboard. In the Attributes inspector for the Table View set the Content to Static Cells and the Style to Grouped.

Subclass a new Table View Controller to manage this table view. Because it’s static table view, you can clean out all the stubbed and commented datasource and delegate methods. It doesn’t generally use these (some can cause problems as it can conflict with information it’s getting from your storyboard), but we will need to implement some methods as you’ll see later. Be sure to set the View Controller on your storyboard to be an instance of your subclassed Table View Controller (via the Identity inspector’s Custom Class).

Using Auto Layout

Our strategy here is that Auto Layout will handle some of the resizing for us, using constraints, and we’ll handle the rest in code. Specifically, Auto Layout is going to handle resizing the UITextView. Auto Layout is going to keep it pinned to four sides of the table view cell.

When working with Auto Layout, remember this two-part rule: for each view you place, you must have constraints in place to determine the view’s position and its size. And not more constraints than are necessary.

These i-beams keep the label pinned to the top and left of the tableview cell.

First drag a UILabel out to your static table view cell. As a static label describing your text, you want it to have a fixed location. As you place it, by default it grabs on to the edges of it’s superview. The I-beams you see are like glue. They will pin, or stick, your label to the top and left a fixed number of points. These two constraints describe the label’s position (per our rule above).

A UILabel, being dragged out of the object library like this, won’t contain any constraints regarding it’s size, its width and height. A UILabel has an intrinsic content size, which is essentially the size of the contents it contains. However, make any changes to the label, for instance, change the text “Label” to “notes:” and suddenly Xcode throws new size constraints in there. To remove the newly added size constraints and go back to the intrinsic size, select the label and choose Editor – > Size to Fit Content.

I mention this because Xcode is VERY touchy about constraints. The slightest movement of views can add or remove constraints. Suddenly you have eight constraints in a small UITableViewCell and it can be hard to tell what’s what. Not to mention that they can conflict with each other and cause crashes.

Here are a couple of pointers when trying to wrangle constraints:

  • In the scene outline view, blue constraints can be deleted, purple ones cannot
  • Xcode adds purple ones to fulfill basic requirements of either size and/or position
  • If you want to delete a purple one because you feel it’s not what you want, you need to add a new constraint to replace it first. To do this, think size, then think position.
  • When trying to think in terms of constraints (versus point-based layout), ask yourself, “What if?” as in “What if the device rotated to landscape, how would I want this button or label to react? Would I want it to grow, stick to the sides?
  • Look closely at the constraints in outline view as you add objects and add constraints to see what Xcode is adding/removing/replacing. This is a great way to learn what’s going on.

Now we need to add a UITextView. Drag it out to your cell and position it to the right of your label. Xcode is going to put a bunch of constraints in there. We want very specific behavior here, so we need to go in and override everything once we’ve placed the text view in the cell.

Let’s think about what we want: for position, we want it to be a fixed distance from the left wall of the cell. And for size, we want it to expand on the top, bottom, and right hand side as the cell gets bigger.

So first, go in and size the text view by hand by setting its Layout Rectangle in the Size Inspector to X: 74 Y: 4 Width: 206 Height: 35 This manual sizing just gives us something to work with. We’re going to go in and override this with constraints.

Next, select the UITextView and Editor->Pin->Leading Space to Superview. Then select the UITextView again and Editor->Pin->Top Space to Superview. Repeat again for Trailing Space to Superview and finally Bottom Space to Superview.

You’ve now given this UITextView enough constraints to have a size and position. Now you need to go in and REMOVE any other constraints Xcode may have added. Delete extraneous constraints until you have the following:

Constraints on our UITextView

Finally, for this UITextView, configure it in Xcode using the Attributes inspector to have a background color of clear color, turn off all the scrollers, disable scrolling, no bounces zoom — you don’t want this thing to move at all when the user is viewing or entering text. You want a hard surface to enter text into.

OK, enough Auto Layout! In your table view controller, create outlets for both your UILabel and your UITextView. You’ll need to reference these in your view controller. Wire them up by ctrl-dragging to your header file.

Resizing Our TextView in Code

UITextView inherits from UIScrollView, which means it has a contentSize property which is basically a rectangle containing the text inside your UITextView.

Our strategy from this point out is to basically look at the text we want to contain in our UITextView, which should be a model object, and calculate how large of a text view we need to contain it. And really, we’re concerned with the height here, because the textview is going to be one of either two widths, depending on if the device is in portrait or landscape mode.

So add the following method in your view controller:

- (CGFloat)heightForTextView:(UITextView*)textView containingString:(NSString*)string
{
    float horizontalPadding = 24;
    float verticalPadding = 16;
    float widthOfTextView = textView.contentSize.width - horizontalPadding;
    float height = [string sizeWithFont:[UIFont systemFontOfSize:kFontSize] constrainedToSize:CGSizeMake(widthOfTextView, 999999.0f) lineBreakMode:NSLineBreakByWordWrapping].height + verticalPadding;

    return height;
}

The UITextView comes out of your storyboard with some bizarre values, so the first thing we do in our view controller is reset its frame and contentSize properties to something form fitting based on the text we want it to contain (by calling the above method). This should be your model, and in the case of a form your model might just hold a placeholder value for now. Here is our -viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // set the model
    self.model = @"The arc of the moral universe is long, but it bends towards justice.";

    // create a rect for the text view so it's the right size coming out of IB. Size it to something that is form fitting to the string in the model.
    float height = [self heightForTextView:self.textView containingString:self.model];
    CGRect textViewRect = CGRectMake(74, 4, kTextViewWidth, height);

    self.textView.frame = textViewRect;

    // now that we've resized the frame properly, let's run this through again to get proper dimensions for the contentSize.

    self.textView.contentSize = CGSizeMake(kTextViewWidth, [self heightForTextView:self.textView containingString:self.model]);

    self.textView.text = self.model;
}

Now, our view controller should be set as the UITextView delegate because it’s going to react when the user types in to the cell: it’s going to grow or shrink the cell whenever they type. So implement -textViewDidChange

- (void) textViewDidChange:(UITextView *)textView
{
    self.model = textView.text;
    [self.tableView beginUpdates];
    [self.tableView endUpdates];

}

This causes the table to recalculate the height for each table cell and it does this by calling the table view data source method tableView: heightForRowAtIndexPath. Here again we calculate a height based on the model using the same method and add a little extra padding in there.

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{

    if (indexPath.section == 0 && indexPath.row == 0) {

        if (self.textView.contentSize.height >= 44) {
            float height = [self heightForTextView:self.textView containingString:self.model];
            return height + 8; // a little extra padding is needed
        }
        else {
            return self.tableView.rowHeight;
        }

    }
    else {
        return self.tableView.rowHeight;
    }
}

It’s important to constantly save your model when editing because you’re constantly calculating your height off of your model. If you have more than one UITextView, in a form for instance, you’ll need to use a series of if or switch statements to identify them by indexPath.

So, run the sample project and you’ll see it loads up the cell sized to fit the text perfectly, and as you add or delete text, everything resizes perfectly and smoothly.

Here is the source code from this sample project: Source Code as Xcode Project

Here's our finished result!

 

12 Responses to “Creating a Self-Sizing UITextView Within a UITableViewCell in iOS 6”

  1. jstewuk says:

    Very nice… if you want to add a line when the user presses return, force the height calculation to calculate with a character on the new line. Add this in heightForTextView:containingString:

    if ([[string substringFromIndex:endIndex] isEqualToString:@”\n”]) {
    string = [string stringByAppendingString:@"B"];
    }

    Thanks

  2. jstewuk says:

    Oops, left out:
    NSUInteger endIndex = [string length] – 1;

  3. Dev says:

    Nice! Helped a lot, but I want a post with the dynamic tableView

  4. Glenn says:

    Thanks so much for this; my scenario was slightly different; trying to auto-size a UILabel with a large block of text.

    The height calculation was easy, but resizing the UILabel in my static prototype cell was not.

    Your solution of adding “pin to top/bottom” constraints and removing the fixed height constraint works perfectly!

  5. Steve says:

    Steve wants you to use Steve’s types – CGFloat, son!

  6. Fang says:

    What does this mean?

    Next, select the UITextView and Editor->Pin->Leading Space to Superview. Then select the UITextView again and Editor->Pin->Top Space to Superview. Repeat again for Trailing Space to Superview and finally Bottom Space to Superview.

    I’m trying to do this but I do not know what you mean by selecting (other than highlighting the UITextView)?

  7. Tedh357 says:

    Is part 2 posted?

  8. Steve Roush says:

    Did part 2 for dynamic TableViews ever get created? If so, what is the link to it?

  9. Alessio says:

    Hi, i would to add this self-sizing tUITextView to my app, but I have a problem: my UITableViewCell doesn’t resize with the text view.

    I’m sure that the code is correct because I’ve just pasted it and I did the constrains several time as you described, but maybe I make some mistake. do you have any idea?

    Thanks

Leave a Reply