Menu & Search

Self-sizing UITextView in a UITableView using Auto Layout (like Reminders.app)

February 7, 2016

I’ve covered dynamically sized UILabels in tables in a previous post but this time, we’re taking a look at self-sizing UITextViews, similar to how the native Reminders app works. Pretty exciting!

Now, I’ve seen a lot of developers make the mistake of overcomplicating their code with custom text size calculations etc. Don’t do that! You do not have to use  textView.sizeThatFits() if you set up Auto Layout properly!

The elegant solution

Step 1

We’re starting from a new Single View project (using Swift) so lets open the Main.storyboard and remove the view controller that Xcode kindly added. Now, lets open the ViewController.swift file and change the ViewController class to become a subclass of UITableViewController instead of UIViewController.

Step 2

Moving back to Main.storyboard, we add a UITableViewController, select the Table View Cell and drop in a UITextView. It’s really important that we set Auto Layout constraints on the newly added Text View properly!

Hot tip: Whenever you want to quickly pin a view to its superview, use this trick:

storyboard-4-constrints

Select your view, then the Pin button at the bottom right corner. On the popover, you can input values for left, top, right and bottom spacings to the super view and by clicking on Add 4 Constraints, you’ll have a nicely pinned view in no time!

PS: Make sure you click on the red constraint marks next to text fields. This toggles constraints you want to add (Xcode selects them automatically if you change the value in the field).

I resized the row’s height to 70 and this is my end result:

Screenshot 2016-02-07 18.07.38

Step 3

Now we need a custom cell with a IBOutlet that will be connected to a UITextView.

(for the sake of simplifying things, I added this 3-liner into the ViewController.swift file, so I don’t need to add a new file)

Go back to Main.storyboard and set your custom cell’s class to MyCell:

Screenshot 2016-02-07 18.16.33

Step 4

We connect the text view to the IBOutlet:

Screenshot 2016-02-07 18.19.15

Step 5

If you disable text view’s Scrolling Enabled property, it will calculate its own intrinsic content size and prevent scrolling (doh). This means it will become aware of the content it holds. Pretty amazing.

stext-view-scrolling-enabled

Step 6

Now for some magic in our View Controller!

The biggest catch to make Auto Layout work nicely with UITableViews is to set table view’s  estimatedRowHeight to a value bigger than zero and  rowHeight to  UITableViewAutomaticDimension

We do this in viewDidLoad:

This will tell our table view that it should respect cell’s calculated size (which is done without our intervention).

For demonstration purposes, I’ve set the delegate code up so that it returns 5 items in 1 section:

For the third function, all I do is to dequeue the cell and set its text view’s delegate to self. We will implement the textViewDidChange  callback that will make sure our table gets properly resized when text changes.

Since our View Controller does not yet conform to the UITextViewDelegate, we add an extension in the same file.

Note: This is where the magic happens. If we’d call tableView.reloadData() it would cause the text view to resign first responder and dismiss the keyboard.

Crappy UX and unusable app!

Instead, we make two calls to the table view:

EDIT: As many of you have noticed, the code above has a UI bug – the table view is jumping and stuttering if you’re editing one of the last rows. I haven’t found the ultimate solution yet, but disabling animations and re-setting the contentOffset of the table view fixes the stuttering. The updated textViewDidChange looks like:

This won’t reload the table but will adjust cell heights if needed. Magic! ⚡️

We’re ready to finally launch the app and see if it works (of course it does).

Code

To see the full sample project, check out the repo on GitHub or download the latest version using this nifty link (zip file).

STOP LOSING TIME WITH AUTO LAYOUT!

Take part in the 5-day course with actionable tasks that will let you become a master at recognizing and solving the most common mistakes iOS developers do with Auto Layout.

Let Auto Layout become a tool you swing with your utmost confidence!

I won't send you spam, I promise. Unsubscribe at any time. Powered by ConvertKit
Hey there! You're already subscribed to my newsletter and you've hopefully gotten some useful tips and tricks when working with iOS. If you're also working with Auto Layout, make sure to check out my book called Auto Layout Fundamentals and get a 20% off for being my subscriber! https://gum.co/autolayoutfundamentals/youareawesome
Jure Zove

A lot of things but mostly a programmer who really likes fast cars. Check me out on Twitter, if you fancy.

Related article

Introducing The “How do I Auto Layout” Cheatsheet

STOP LOSING TIME WITH AUTO LAYOUT! Take part in the…

Fastest way to use Auto Layout in code

Raise your ✋ if you hate adding views and setting…

Swift Optionals Demystified

STOP LOSING TIME WITH AUTO LAYOUT! Take part in the…

  • Gerwazy Sokołowski

    Excellent tutorial, thank you so much for this, however I’m trying to implement exact same functionality in a tableView with static cells and it does not seem to work. textViewDidChange method is being called with no problems whatsoever but the cell height does not react to the changes in textView height. Is there something I should edit to make your solution work with static cells? Would really appreciate the help.

    • Hey Gerwazy! Glad you found the solution! Does it work if you return UITableViewAutomaticDimension in heightForRowAtIndexPath and some number in estimatedHeightForRowAtIndexPath?

      • Gerwazy Sokołowski

        Yep, for some unknown reason and according to a somewhat related SO thread I was able to google since iOS 8 using properties does work for dynamic cells only and for static cells one must implement those delegate methods. So all I did was return the values you’ve provided above from those methods and it did the trick.

  • Егор Меркушев

    Your solution has a bug. See it: https://www.dropbox.com/s/ksl9mp809rfn0l4/textview-uitableview-2-480.mov?dl=0 When textViewDidChange UITextView send to upper UIScrollView some private messages and it calling cause this trembling of content offset. See also https://www.dropbox.com/s/92oyu87v35f9x9h/textview-uitableview-480.mov?dl=0

    • Егор Меркушев

      Working solution without calculation height (using Auto Layout & UITableViewAutomaticDimension) – http://stackoverflow.com/a/18818036/602249

    • Erop, thanks for pointing this out, haven’t seen the issue before! Will review the SO post you mentioned and make adjustments. Did the solution you found work for your use case?

      • Егор Меркушев

        Yes, I have working solution. But it’s more complex for this comment:
        1) view controller handles keyboard’s appear and change bottom in contentInset of table view for shorter vertical scroll
        2) cell that is delegate for subviewed UITextView handles -textViewDidBeginEditing:, -textViewDidEndEditing:, -textViewDidChange: & -textView:shouldChangeTextInRange:replacementText:
        There is a lot of code.
        Key moment is working in -textViewDidChange:. First, I fix scroll jumping by set saved content offset without animations:
        // fix jumping
        if (self.tableView.contentOffset.y < self.prevChangesContentOffset.y) {
        [UIView performWithoutAnimation:^{
        self.tableView.contentOffset = self.prevChangesContentOffset;
        }];
        }
        then I check textview's height and height of result of sizeThatFits for this textview and then I use some hack to stop table view quacking:
        -(void)reportSizeChanged
        {
        [UIView performWithoutAnimation:^{
        [self.tableView beginUpdates];
        CGPoint savedOffset = self.tableView.contentOffset;
        [self.tableView endUpdates];
        [self.tableView setContentOffset:savedOffset]; //restore offset
        }];
        }
        It is short story about my workaround…

  • paul

    I have a tableviewcell with an image to the left, two labels to its right and a label below them all.

    I have tried to do what you do with row height and estimated but all I get is default 44 row height and a warning:

    Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a tableview cell’s content view. We’re considering the collapse unintentional and using standard height instead.

    any ideas? Thanks

    • Hey Paul

      Can you check if the tableView is instantiated where you set the rowHeight and estimatedRowHeight? I’ve seen this error appearing where these two properties were not set up on the correct table view or the table view was not properly linked from the IB.

      • paul

        Hi, thanks for reply 🙂

        I double checked and yes tableView is all linked correctly.

        I have even stripped everything out of cell accept one label set with leading,top,trailing layouts and lines set to 0 but still default row and that warning.

        In cellforrow I am finding label with viewWithTag…could that be it?

        • Does the label have a bottom constraint as well?

          • paul

            AWESOME!!!!! :-)) that worked!

            So if I now reset to imageview on left which has fixed size 200×200 then two labels to right and then last at bottom do I have to attach contraints from all to contentview or to each other?

          • That’s great! 🙂 Glad to help!

  • Rameez Hussain

    Excellent post! Saved me lot of time and works like a charm!

  • Alex Radford

    Hey, I was just wondering how I would reference the TextView in code to set it’s text. Tells me I can’t set an outlet to a repeating textView. Thanks!

    • Hey Alex

      I suspect you’re trying to connect your text view in the cell to the view controller. You can’t do that – you should connect the text view to your custom cell class. In the case above that’s MyCell. Does that help?

      Thanks

  • akozin

    Thanks you very much!
    It works great!
    I spent half a day in the search for solutions.

  • Mohamed Yaseen

    is this solution is possible when we are using custom XIB instead of Prototype cells ???

    • Of course, same principle, you just have to link your XIB to the code instead of the prototype cell in the main storyboard!

  • Robert N

    I’m trying this on iOS 10 at the moment. It works but only if I remove the lines let currentOffset ... and tableView.setCurrentOffset ... If I leave those lines it behaves badly. Jumps around, views in my cell disappear. Weird. I need to find an iOS 9 simulator to see if there’s a difference.

  • Martin Plus

    Here is my solution:

    func textViewDidChange(_ textView: UITextView) {
    UIView.setAnimationsEnabled(false)
    tableView.beginUpdates()
    tableView.endUpdates()
    UIView.setAnimationsEnabled(true)
    tableView.scrollRectToVisible(textView.convert(textView.bounds, to: tableView), animated: true)
    }

    Cheers! 🙂

Type your search keyword, and press enter to search