Fancy Footwork with iOS 8

While working on my sooper-sekrit project today, I came across a surprising hurdle. I wanted to accomplish the following feats using Interface Builder inside Xcode 6 (running iOS 8):

  • A fixed-size UITableView, one that doesn’t scroll, but rather alters its height in its superview as the contents change;
  • The table view has rows of varying height;
  • Inside a UIScrollView that contains other views, above the table view, such that the full view, including the table view, scroll together;
  • Using AutoLayout and iOS 8′s new Size Classes

Turns out that it wasn’t easy! But with the help of Stack Overflow, various blog posts and some elbow grease, I got it done. Let’s take a tour.

TL;DR: This is all encapsulated in a demo project, which I’ve posted on Github.

Screen Shot 2014 08 14 at 9 43 10 PM

Layout In Interface Builder

As Apple outlines in TN2154, Auto Layout works a bit differently when scroll views are involved. In a nut, while Apple talks about a “mixed” vs “pure” approach to setting your constraints, I recommend the former. When putting content inside a scrollview, embed the content into a separate UIView class first. That way, you only need to adjust the constraints on the view.

In the sample project, take note of the constraints that match the width of the container view to that of the scroll view. This is the only way I was able to avoid an ambiguous scrollview warning; constraining the leading and trailing edges isn’t sufficient.

In IB, I’m also setting two properties as constraints: the height of the container view, and the height of the table view. At runtime, I’m going to reset the height constants on both properties in order to accommodate the change in row heights.

The table view cell contains a UILabel that is constrained to each edge of the cell, so it will expand. Set the UILabel numberOfLines to 0 and turn on Word Wrap. In code, we’ll have to set the preferredMaxLayoutWidth, so it knows where to wrap the label. We’ll get to that shortly.

Setting up the View Controller

In the VC’s viewDidLoad: function, there are a pair of important lines required in iOS 8 to manage variable row height:

self.tableView.estimatedRowHeight = 80
self.tableView.rowHeight = UITableViewAutomaticDimension

The estimatedRowHeight property should be set to whatever the most common row height might be. This doesn’t have to be accurate all the time. The table view’s rowHeight property tells the table to calculate row heights using Auto Layout.

The view controller in this example is also the table view’s data source, and the noteworthy line is in the cellForRowAtIndexPath function:

cell.textContentLabel.preferredMaxLayoutWidth = self.tableView.bounds.size.width - 16

Auto Layout requires this value to know where to wrap the text. Without an accurate value here, you’ll get some pretty whacky results!

Finally, there’s the viewDidLayoutSubviews() function. There’s some crazy stuff in here, but it’s the result of a great deal of trial and error.

The purpose of this function is to set the new height constraints on the table view and the container view (setting the height of the container view with Auto Layout will trigger a change in the enclosing scroll view’s contentSize property, enabling accurate scrolling). Throughout this function I’m calling layoutIfNeeded(), to ensure that all objects are being accurately represented. The whole thing is enclosed in a dispatch_async block. Why? Because otherwise it doesn’t work right, dammit. That’s why.

Testing it out

The end result is a view that has a fixed table height which scrolls along with the image above. You can rotate the device and the table will resize correctly. You can open the thing on the iPad simulator, and it still works. I’ve also added support for Dynamic Type, so you can change to a larger version of the text, and the table will reload accordingly.

I hope you find it useful! Code on, my friends.