I Can’t Believe It’s Not Butter

Jenni
SEEK blog
Published in
5 min readSep 6, 2016
Butter scrolls <food.ndtv.com>

Have you ever wondered why some apps have lists that scroll smoothly while others don’t? While working on the search results code in the SEEK iOS app, we found that the answer is straight forward but it took us a while to wade through all the factors which combine to determine scroll performance.

What is “scrolling smoothly”?

While smooth scrolling is definitely subjective, it is generally quantifiable in iOS as 60 frames per second. This means that while scrolling, your app has to spend 16.7ms or less at a time on the main thread. Due to operating system overhead, realistically your app has a lot less than 16.7ms. So the short answer is if your app spends even close to 16.7ms on the main thread, your scrolling performance will suffer.

Source

iOS UITableViewController

If you search for answers on how to make your app scroll smoothly in iOS, you will likely run into this list of DON’T’s:

  • Don’t use variable height cells
  • Don’t use AutoLayout

The main control that developers use in iOS for vertical lists is UITableView. Simply put, in order for you to render a list of scrollable cells, you must implement 2 methods for the UITableView controller:

  • A method which returns how tall any particular cell is
  • A method which returns a particular cell, with laid out controls

The first don’t is a way to avoid the repeated computation required for the first method, which returns the cell height. If all cells are the same height, then no repeated computation is required. In fact, using a constant might be possible.

The second don’t is a way to reduce the computation time required for the second method, which lays out the controls. The reason for this is that while AutoLayout is powerful and flexible, it can be slow. AutoLayout is expressed as constraints, which at run time, must be satisfied in order for the layout to be completed. Unfortunately, the computation time of AutoLayout increases nonlinearly as the view complexity increases. View complexity is a function of the number of sub views as well as their constraint relationships.

What the heck?

So first of all, fixed height cells probably work fine in some scenarios but there are plenty of others where that is not an option. Secondly, AutoLayout is supposed to be the way forward, freeing developers from having to painstakingly code and maintain UI code.

So now what?

For the SEEK iOS app, we knew that we could always go back to manual frame layout code which gives us acceptable performance even for variable height cells. The problem is that the manual frame layout code comes with the cost of lots of fiddly code. Gone would be the nice visual XIB files, replaced by lots of error prone manual calculation code.

If we did that we probably wouldn’t be posting about it, so to make a long story short, we didn’t give up and went through a journey that took us to writing our own layout framework and while it is frame based, it greatly reduces the amount of code required compared to standard frame layout code.

Stackable

When iOS 9 was released, one of the really promising additions was UIStackView. Seemingly inspired by Flexbox from the CSS world, it made expressing hierarchical views very easy compared to using AutoLayout constraints. Unfortunately, while we loved the expression of views in UIStackView, it is still based on AutoLayout which means that performance is not acceptable for UITableView.

Stackable is our interpretation of Flexbox/UIStackView style view expression which is built upon frame layout.

Here is an example:

let descriptionLabel = UILabel()
let attribute1 = UILabel()
let attribute2 = UILabel()
let attribute3 = UILabel()
let logoImageView = UIImageView()

let stack = VStack(spacing: 2, thingsToStack: [
HStack(spacing: 10, thingsToStack: [
VStack(spacing: 1, thingsToStack: [
attribute1,
attribute2,
attribute3
]),
logoImageView.stackSize(100, 100)
])
descriptionLabel
])

let width = self.frame.size.width

// give me the frames
let stackedFrames = stack.framesForLayout(width)

// how tall is the stack?
let height = stack.heightForFrames(stackedFrames)

// lay the frames out
stack.layoutWithFrames(stackedFrames)

In general, Stackable is a declarative iOS view layout framework which at it’s core, reduces the hierarchically expressed views into a flat array of frames. These frames are immutable and cacheable which means that the layout computation only needs to occur once for a given cell. These frames can then be used for both the previously mentioned methods without incurring any additional layout computation penalty:

  • A method which returns how tall any particular cell is
  • A method which returns a particular cell, with laid out controls

Additionally, this approach is compatible with the recommended way of reusing cells in UITableView where only a handful of actual cells are instantiated at a given time to reduce memory usage.

Other useful things we found along the way

Sometimes poor UI performance is only a reflection of something else. For instance, due to a backend change, some code which generated view models went from being cheap to being expensive. In that particular case it only added about 2ms per cell view model but that was enough to tip scroll performance over the edge (even with manual frame layout code). The fix for this problem was to perform the view model generation on a background thread.

How are we using Stackable?

Stackable is currently being used in the SEEK iOS app search results view. While there is a visible stutter when more search results are merged into the table view as the view is scrolled, we can’t see any of the stuttering while scrolling in general which we saw previously with an AutoLayout based view. Going forward we would like to use it in more of our UITableView based views.

Why the post?

Sometimes it’s nice to do a search for a solution to a problem and see some DO’s instead of DON’Ts. It’s also also why we made Stackable open source and also a cocoapod. While we don’t think it’s the solution for every layout performance issue we think it is general enough and extensible enough that it might help others, if not as code then maybe as an idea.

github:
https://github.com/SEEK-Jobs/seek-stackable

cocoapods:
https://cocoapods.org/pods/Stackable

Written by Ronny Chan, Senior Developer at SEEK

--

--