This Swift Language User Group (SLUG) talk discusses Auto Layout, Apple’s recommended technology for building UIs on their platforms. LinkedIn software engineer Nick Snyder describes how the company uses a lot of Auto Layout, but after running into some frustrating performance problems, decided to build and open-source their own solution called LayoutKit. Not only is LayoutKit much faster than Auto Layout, but it is also easier to use because layouts are declarative, immutable, and thread-safe.
Introduction (0:00)
Hello, my name’s Nick Snyder and I’m a software engineer at LinkedIn. Today I want to tell you a story about Auto Layout, how we use it at LinkedIn, some problems we’ve encountered using Auto Layout, why we decided to stop using it in some places, and what we are using instead.
I want to start with the good news about Auto Layout. We use a lot of it at LinkedIn. It’s our primary way for building UIs for all of our apps. Auto Layout is powerful. It makes it easy to support things like multiple screen sizes and right-to-left languages.
The bad news is, Auto Layout performance is not good enough in some situations. It doesn’t scale to complicated view hierarchies. We’ve observed performance regressions on certain iOS releases, and it’s unpredictably bad for some layouts.
Auto layout problems (1:09)
Here’s an example of a layout in the LinkedIn app. It has two labels: there’s a multi-line label on the left, and a single line label on the right. For the right label, we just want it to be as big as it needs to be to display the content. Think of it as a badge with a number. Then the left label just has some arbitrary amount of text that we want to wrap to up to two lines.
To accomplish this using Auto Layout, on the right label we give it required content hugging and required content compression resistance, so that it’s exactly the width that it needs to be. This worked fine in iOS 8, and it worked fine with all the sample data that we tested this layout with during development. But when iOS 9 launched, this caused a huge performance problem for some of our users that we didn’t know about until they started complaining.
Obviously it’s not great to be notified by our users of performance problems. You might think, “Well, how bad could it be?” Well, for this situation, where the left layout has multiple lines, the blue line shows how long it takes to perform layout when you have a certain number of views. As you can see, the blue line shoots up really quickly as the number of views increase. This is where our performance was really tanking.
This problem does not happen if the label on the left only has one line or some other type of data in it. So it was a very specific type of data that was causing this problem.
For the LinkedIn news feed, we’ve actually known that Auto Layout doesn’t have great performance. The news feed on LinkedIn app has not used Auto Layout for a very long time because of this. In the LinkedIn news feed, each view or cell implements its own layout code using layoutSubviews
.
Doing this manual layout is significantly better than Auto Layout. The problem is that it’s tedious to maintain. We have two different functions. One of them calculates the height, so we can tell the table view or a collection view what height the cell is, and then the other does the actual layout. The reason we separate this logic is so that we can do height calculations quickly without doing the full layout.
So we wanted something similar in other parts of the app, but we wanted it to be reusable and shareable.
Layout solutions (2:09)
We wanted this solution to be fast. It needed to perform on par with writing layout code manually, because that’s what we were already doing on the LinkedIn feed. We wanted an API that was natural to use in the Swift app. At LinkedIn, most of our new apps are written in Swift, including the main LinkedIn app. If we were going to use a third-party solution, we needed to make sure that it was maintained and had non-trivial adoption. We don’t want to use beta software for our production apps.
We also wanted it to be open-source. One of the pain points with Auto Layout is that it’s a black box, and when things went wrong, we didn’t have any ability to dig into it and figure out why. Also, if we were going to use an open-source project, the license had to be approved by our lawyers.
What were some available solutions? Facebook has a lot of nice open-source libraries: React Native, AsyncDisplayKit, ComponentKit. Unfortunately, we can’t use those at LinkedIn. There was Few, which was a library, but it doesn’t look like it’s maintained; its last commit was over a year ago. There’s also a library we found called Render, which didn’t exist at the time that we made this decision. It was created in May this year.
None of the projects that we found online satisfied all our requirements. It was time for us to build something new. We built something called LayoutKit.
What is LayoutKit? It’s a fast view layout library for iOS, MacOS, and tvOS. For the next part of the talk I want to go over how to use it and how it works.
LayoutKit hello world (5:42)
At a high level, layout is done in three steps:
- A developer defines a layout using immutable data structures.
- LayoutKit computes the view frames, on a background thread if you want.
- LayoutKit creates views and assigns the frames to those views on the main thread.
To learn more about how LayoutKit works, we’re going to walk through an example, and this is going to be the end result: a simple layout with a world and an image and a text label.
For the first part of this layout, we’re going to create the image view. This is just a fixed size layout called a SizeLayout
with a view type of UIImage
with a width and height of 50 pixels. In the configuration block we set the image that we want on the image view.
let image = SizeLayout<UIImageView>(
width: 50,
height: 50,
config: { imageView in
imageView.image = UIImage(named: “earth.jpg”)
}
)
Next, we have the label layout. We assign text to it and we give it an alignment. This says that it will center in the available space.
let label = LabelLayout(
text: “Hello World!”,
alignment: .center
)
We want to arrange these views next to each other, so we create a horizontal stack with four pixels of spacing.
let stack = StackLayout(
axis: .horizontal,
spacing: 4,
sublayouts: [image, label]
)
Finally, we want padding around the edges. We create an InsetLayout
that wraps the stack layout we just created.
let helloWorld = InsetLayout(
insets: UIEdgeInsets(top: 4, left: 4,
bottom: 4, right: 8),
sublayout: stack
)
Now that we have our “Hello World” layout, we call the arrangement method. This method calculates all of the frames for all of the views and layouts recursively. This can be done on a background thread.
Once we have the arrangement, it’s an immutable data structure, so we can pass it back to the main thread and then call makeViews
.
// May be done on a background thread.
let arrangement = helloWorld.arrangement()
// Must be done on the main thread.
arrangement.makeViews(in: rootView)
We give the makeViews
method a rootView
to instantiate the views inside. If we don’t do that, it will return a view with which we can do with whatever we want.
And we get our layout. For this example, we called arrangement
with no parameters.
// Intrinsic size with no constraints.
helloWorld.arrangement()
I want to go over another example.
// Explicit constraint on width.
helloWorld.arrangement(width: 200)
You can give an explicit width and it will perform the layout given that width. You can see that the padding increased: it took up the entire width that was available to it. For example, this width might be your device screen width.
You can also animate layouts. To do this, we’re going to use a simple size layout; we’ll call it a box
. Use the viewReuseId
parameter and this gives the view or layout a unique ID, so LayoutKit knows which view in the before state corresponds to which view in the after state, and then it can automatically animate between those states.
We have a before
state here, just a 50x50-pixel box, and an after
state, which is a 25x25-pixel box.
// Give animated layouts a viewReuseId
let before = SizeLayout(
width: 50, height: 50, viewReuseId: “box”)
let after = SizeLayout(
width: 25, height: 25, viewReuseId: “box”)
These are two different layouts. We create our view arrangement with the before
layout, and we make our views in some root view. Then we prepare for an animation using our after
layout. This animation is an object with an animate
method.
// Initial layout.
before.arrangement().makeViews(in: rootView)
// Prepare the animation.
let animation = after.arrangement()
.prepareAnimation(for: rootView)
After you have this animation object, you can do your normal UIView.animate
, withDuration
, and pass it the animation.apply
method which will perform the actual animations from one layout to another.
// Perform the animation.
UIView.animate(withDuration: 5.0,
animations: animation.apply)
Here’s a simple example where we have two gray boxes and a red box that is animating. One thing to note in this example is the red box actually starts as a subview of the top box and then transitions to a subview of the bottom box. So both boxes are moving left to right, and the bottom box is shrinking while the red box is increasing in size while it’s being re-parented to another view. The “prepare for animation” step is what allows this complex view re-parenting to work. If you don’t do this, it won’t do what you think it should do.
How does LayoutKit work? (9:56)
Now I want to talk about the benefits of Swift. LayoutKit is written in Swift and it takes advantage of Swift features to provide a clean API. We use generics, protocol extensions, and initializer parameters with default values. You saw the benefits of these features when creating the layouts in the example.
How does LayoutKit work? At the center of LayoutKit is a layout protocol, and this defines what a layout is. Anyone can implement this layout protocol, and LayoutKit provides a small set of pre-built layouts that implement this layout protocol.
Finally, there’s the layout engine which is a bunch of classes that operate on the layout protocol itself to compute the size of the frames and instantiate them and perform the animation logic.
I want to go over the basic layouts, some of which we’ve already seen. These layouts are pretty simple: it’s just math packaged in a layout protocol. We have a label layout, we have a size layout, an inset layout, and a stack layout. These are the fundamental building blocks for building a decent number of UIs.
As I mentioned, you can also create custom layouts if you have special layouts or if you can’t express your UI using the four basic ones. We found that most of our layouts can be expressed using those basic layouts.
Why is LayoutKit fast? It’s not a generic constraint solver like Auto Layout. Each layout has its own specialized algorithm, so you can take advantage of the characteristics of that layout to perform the layout efficiently. The slowest thing we do in LayoutKit is sorting sub-layouts by flexibility in stack layout. Everything else runs in linear time.
Let’s look at some actual performance numbers. This is an example running on an iPhone 6 with iOS 9. It’s a 20-cell UICollectionView
using UICollectionViewFlowLayout
, and each cell is something representative of what a LinkedIn news feed item would look like. Iit’s a pretty big portion of the screen.
In this graph, higher is better. We have Auto Layout as our baseline, 1x. You can see that if you use UIStackView
, it’s actually slower than Auto Layout because it’s built on top of Auto Layout. On the right, we have manual layout. Manual layout is 9.4 times faster than Auto Layout. On the green we have LayoutKit, and LayoutKit is about 7.7 times faster than Auto Layout. Not as fast as doing manual layout, but you get a lot of nice things without having to write a lot of code.
Here’s another way to look at it: how much work can you do in one frame? The black horizontal line is 16 milliseconds and you can see that for a feed item using UIStackView
, it would take 46 milliseconds to perform layout. Using Auto Layout it would take 28 milliseconds. LayoutKit and manual layout are basically the same. What this shows you is that if you’re using Auto Layout or UIStackView
, you’re dropping one or more frames every time you do layout on the main thread. If you use manual layout or LayoutKit, it’s only 6 milliseconds, and also with LayoutKit you can do that in a background thread.
The next thing I want to talk about is immutable data structures. LayoutKit uses immutable data structures for layout object themselves as well as all intermediate data structures. This gives us thread safety and allows us to pass data back and forth between the background thread and the main thread. It also makes it easy to cache or pre-compute layouts. For example, you could pre-compute what your rotation layout would be in the background before the user actually rotates their device. It’s also easy to debug. If you know a variable isn’t going to change, then you don’t have to worry about checking it.
LayoutKit also has some other benefits. It handles right-to-left languages automatically. We found that declarative layouts are easy to read, write, compose, and test, and you can prototype layouts pretty quickly in Playgrounds which is nice. LayoutKit also has image enforce, supports iOS, MacOS, and tvOS.
Is LayoutKit production ready? Yes, we use it in the main LinkedIn app, certain screens including profile, and we also use it in our job search app. Our experiences with onboarding engineers to learn LayoutKit has been pretty easy. Conversely, onboarding engineers onto Auto Layout hasn’t been as easy.
Conclusion (15:03)
It is open-source, so you can go to layoutkit.org to check it out. It’s an Apache 2 license, so no patent shenanigans. It was released in June of 2016 and today we have a pretty healthy project. People are filing issues and contributing code and that’s great. If you’re interested in helping out, check it out.
I want to say thanks to everyone who helped work on LayoutKit. Thank you Sergei, Andy, and Peter. That’s it.
Receive news and updates from Realm straight to your inbox