The auto-resizing UITableViewCell has been more of a myth than a reality. I've seen it deployed in apps but for the life of iOS7 I considered it my personal Auto Layout white whale. I estimated my row heights. I calculated my row heights. My row heights were never, ever correct. So I gave up.
iOS8 has promised to make this flummoxing process "easier" than its predecessor. There's even a WWDC session (What's New in Table and Collection Views) to show you the way. I watched the video, I read the documentation, and I still had a buttload of trouble getting auto-resizing to work. But I did eventually get it to work and I'll show you how, nice and slowly, below.
A Note on Using Interface Builder
Don't. I know writing constraints in code is a pain in the ass but I've whittled them down here to be as simple as I possibly could make them, using only the almost-human
Our UITableViewCell Subclass: MagicTableViewCell
I have a specific need for auto-resizing UITableViewCells: The bookmark cells in Pinline. I need to display three pieces of information in each cell, two of which would benefit from being resizable:
- Title (Variable)
- URL (Usually okay to truncate)
- Tags (Variable)
So I need a cell with three UILabels. I want each of them to expand horizontally to almost the width of the cell. I want them to be stacked, with a little distance between them, and I want the title and tags labels to expand vertically to fit their content. I'll use a single method to fill in these labels with a
Bookmark object that just contains the three strings I need as properties.
And here's our
First up we create three properties for our labels. I like to put these in the .m so that I can be sure they're only populated from methods within the class. Pretty standard stuff:
Next, in our implementation, we override this crucial class method:
This tells our app that we'll be applying our own constraints for the content view of the cell later on in the
updateConstraints method. This is an important piece of Auto Layout magic and forgetting to override this is easy.
Now we override our initializer to include the code that creates our labels and adds them to the cell:
Some important points to note here:
- Remember to set
translatesAutoresizingMaskIntoConstraints = NOon every subview.
preferredFontForTextStyle:to support Dynamic Type.
numberOfLines = 0on any UILabel you want to automatically expand to fit its content.
- Always add subviews to the
contentViewof the cell and never to the cell itself.
Next up we lay out our constraints. NSLayoutConstraints are confusing. A great way to get started using them is the
constraintsWithVisualFormat: method, which takes a formatted string that kind-of-sort-of looks like the layout you're trying to achieve and then spits out constraints to describe that layout.
First, we create an
NSMutableArray to hold the constraints we'll be making. Then we create an
NSDictionary of our views using the incredibly convenient
On to the constraints themselves. First we want to set constraints that put each of the labels 15 points away from the left and right edges of the cell's content view. The string you give to
constraintsWithVisualFormat: for that looks like this:
H: means that we're working horizontally. The
| characters represent the superview, in this case, the left and right edges. The
-15- means "a distance of 15 points." Finally,
[_theNameOfMyView] represents our view. When you put it all together,
@"H:|-15-[_theNameOfMyView]-15-|" means "my view should always be fifteen points away from its superview on its left and right edges."
We want this constraint on all three of our labels:
Now we want to lay out our labels vertically. This can all be knocked out in one line:
@"V:|-[_titleLabel]-[_urlLabel]-[_tagsLabel]-|". This time we're working vertically, so we use
| again represents our superview, this time the top and bottom edges. The views are in brackets. I'd like the labels to be pretty close vertically so instead of specifying a point size I just use a single
- between them, which means "a recommended standard amount of space." This is the same distance you'd get using the snap guides in Interface Builder.
We wrap up the method by adding our array of constraints to the content view of the cell. We then call
[super updateConstraints] to tell Auto Layout we're done.
Finally we have our little convenience method that populates the labels with text:
That's it for our
MagicTableViewCell subclass! Before we're done, though, there are a couple things you need to remember to do your
MagicTableViewController to make it support your auto-sizing cell:
- Delete any prototype cells you have in your Storyboard, if you're using one.
viewDidLoadremember to register your custom class with the table view:
[self.tableView registerClass:[MagicTableViewCell class] forCellReuseIdentifier:@"Cell"];
Aside: UILabels and Rotation
There are some issues with UILabel in the latest SDK that can cause a label to not properly resize when the device is rotated. I went through a bunch of different techniques to fix this (which I'll save for their own post) but in the end the fairly unsatisfactory answer I came up with is to just reload the table view when your device rotates. You can do that in your TableViewController like so:
I hope the fruit of my struggles with auto-resizing cells will help save you from your own. If there are any details you think you might still be missing, you can download my complete demo application from Github and tear it apart yourself.
But wait, what about Swift?
Alright, alright. I spent a little time converting the classes in the demo app to Swift. It's a pretty straightforward rewrite of the Objective-C code and I didn't use any particularly tricky Swift features.
Edited: Sept 24, 2014