UIStackView:- A Public Service announcement

TL;DR Please stop using UIStackView to build complex reusable views.

UIStackView was released in iOS 9 and it is a marvellous thing. With just a single click we can add to and edit our views and barely even need to think about autolayout. Adding and removing arranged subviews is gloriously easy in code and we even get free animations for the insertion and deletion of an arranged subview. When working in interface builder we even get a handy little button which converts a selection of UIViews into a UIStackView and makes all our autolayout woes disappear. We can even nest UIStackViews allowing us to build some really complex layouts easily. It just works like magic.

Performance

But magic comes at a price and that price is performance. To make all this magic happen comes with a significant amount of overhead. In most cases this overhead is manageable and does not cause any issues. Rendering a complex UIStackView arrangement happens in such a way that our users will never notice the overhead, and in many situations a UIStackView is actually the right tool for the job.

When UIStackViews get dangerous

Unfortunately, a very common scenario for iOS developers leads to issues when we attempt to use a UIStackView, even though it seems like the perfect tool for the job. That is reusable views (particularly cells) with very dynamic content. We all know these types of cell, they often start off very simple, but as requirements evolve they grow and grow and grow. What we are left with is a cell that based on the data contained in the model it is supplied with can be configured in a multitude of different ways.

These complex cells nearly always have some very complex autolayout logic going on. Enter UIStackView, the temptation is to0 much, interface builder is practically begging you to use and abuse UIStackView and before you know it this cell is full of them. I have come onto a code base recently that had UIStackViews nested six layers deep! This is dangerous territory.

The problem is that rendering this vast number of UIStackViews each with their own performance overhead makes cell rendering slower and it doesn't take much to push your table or collection view over the edge. Very quickly you will see jittering as you scroll, we aren't hitting that all important 60fps and it starts to show. This really makes your app look amateur and cheap.

To demonstrate this problem in more detail I have created the same cell featuring just a few UI components twice, one using UIStackViews and the other avoiding them completely. In both cases the cell looks as follows;

The cell is still very simple but is enough to demonstrate my point. The layout for cell implementing UIStackViews is pictured below;

The non UIStackView cell is also pictured for reference.

In order to get a rough idea of how the two compare I ran the following performance test


class StackViewBenchmarkingTests: XCTestCase {
  let n = 1000

  func testStackViewCell() {
    self.measure {
        let cell = StackViewTableViewCell.loadFromNib()
        self.sizeCell(cell)
      }
  }

  func testNonStackViewCell() {
    self.measure {
      let cell = NonStackViewTableViewCell.loadFromNib()
      self.sizeCell(cell)
    }
  }

  func sizeCell(_ cell: UITableViewCell) {
    for _ in 0..< self.n {
      cell.contentView.translatesAutoresizingMaskIntoConstraints = true
      cell.contentView.widthAnchor.constraint(equalToConstant: 320).isActive = true
      cell.contentView.setNeedsLayout()
      cell.contentView.layoutIfNeeded()
      _ = cell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
    }
  }
}

Where n was a positive integer, running the test for values of n starting at 100 and incrementing by 100 up to 1000 (running over a range of numbers to help minimise the impact of inaccuracies of this kind of testing) yielded the following results;

The results reveal that even in this relatively simple case with only a few UIStackViews being used the cell using UIStackView takes on average 1.7 times as long to render (σ = 0.05) as the cell created using normal autolayout.

While these tests are not the most accurate readings I have ever taken, I include them to help demonstrate the point that there is a performance overhead of using a UIStackView. If there is a lot more going on in cell configuration this WILL cause your app to start dropping frames unnecessarily.

In addition, over reliance on UIStackView can often create a rare and hard to reproduce bug. Very occasionally autolayout won't be able to calculate the cells arrangement on time and will just collapse one or more of the UIStackViews in the cell. This is incredibly time consuming to track down, by not using a UIStackView you can save yourself a great deal of effort.

In conclusion

I am not saying never use a UIStackView in a reusable view, just think before you go mad with it. Is it really the best way to solve your problem or just the easiest right now?

In the follow up blog post Dealing with Dynamic Cells I will discuss techniques for handling dynamic content in cells highlighting the pros and cons of each.