User interfaces can be hard to get right in any application. Combining display and interaction in a little rectangle on the user's screen seems simple, but even for small applications, it's easy to end up with a tangled mess of view code. In complex products with many contributing engineers, like Facebook's News Feed, these views can be especially hard to develop and maintain over time.
Recently I've been working on a library called Components to make views simpler on iOS. It emphasizes a one-way data flow from immutable models to immutable "components" that describe how views should be configured. It's heavily inspired by the React Javascript library that has become popular on the web. Just like React, which abstracts away manipulation of the DOM using a concept called the "virtual DOM," Components abstracts away direct manipulation of UIView
hierarchies.
In this post, I'll focus on some of the benefits of switching to Components for rendering News Feed on iOS and share lessons I've learned that you can apply to your own apps.
No Layout Math
Suppose we have four subviews and want to stack them vertically, stretching them the full width horizontally. The classic way of doing this is to implement -layoutSubviews
and -sizeThatFits:
, which clocks in at around 52 lines of code. There's a bunch of math and it's not immediately obvious at first glance that the code is vertically stacking views. There is a lot of duplication between the two methods, so it's easy for them to get out of sync in future refactors.
If we switch to Apple's Auto Layout APIs, we can do a little better: 34 lines of code. There is no longer any math or duplication — hurrah! But we've traded that for a different set of problems: Auto Layout is hard to set up, 1 is difficult to debug, 2 and suffers from poor runtime performance on complex hierarchies. 3
Components draws inspiration from the CSS Flexbox specification for its layout system. I won't get into the nuts and bolts; check out Mozilla's fine tutorial to learn more. Flexbox makes layout so much easier that the equivalent in Components weighs in at only 18 lines of code. There is no math and no string-based visual format language.
Here's how you'd do the same vertically stacked layout in Components. To the unfamiliar eye, the syntax looks pretty weird — it'll be explained shortly:
@implementation FBStoryComponent
+ (instancetype)newWithStory:(FBStory *)story
{
return [super newWithComponent:
[FBStackLayoutComponent
newWithView:{}
size:{}
style:{.alignItems = FBStackLayoutAlignItemsStretch}
children:{
{[FBHeaderComponent newWithStory:story]},
{[FBMessageComponent newWithStory:story]},
{[FBAttachmentComponent newWithStory:story]},
{[FBLikeBarComponent newWithStory:story]},
}]];
}
@end
But All Those Braces!
Right. We use Objective-C++. Aggregate initialization gives us a great way to specify style structs in a terse and type-safe manner. Here are a few examples of other valid style:
values:
style:{} // default values
style:{.justifyContent = FBStackLayoutJustifyContentCenter}
style:{
.direction = FBStackLayoutDirectionHorizontal,
.spacing = 10,
}
Using STL containers like std::vector
and std::unordered_map
give us more type safety than Objective-C's corresponding containers. We also get the benefits of stack allocation for temporary view model data structures, boosting performance.
There are some other stylistic oddities in Components (the use of +newWith...
instead of -initWith...
for brevity, nonstandard indentation) that make sense with more context — a subject for another blog post. Back to the good stuff.
Declarative, Not Imperative
Even with the completely new syntax, it is pretty easy to observe what is happening in the Components version of our stacking view. There's one key reason why: it's declarative, not imperative.
Most iOS view code reads like a series of instructions:
-
Create a header view.
-
Store it to the
_headerView
ivar. -
Add it to the view.
-
Add constraints that equate the header's left and right sides to the superview's.
-
... do similarly for other views
-
Add more constraints that stack the views.
Components code is declarative:
-
A story is rendered with these four components stacked vertically and stretched horizontally.
Think about the distinction as the difference between a list of materials and directions to workers, and a full blueprint. To stretch the analogy a bit, the architect shouldn't run around a building site telling the workers exactly how to do their jobs — it would be far too chaotic. Declarative techniques focus on what needs to be done, not how it should be done; as a result, you can focus on intentions instead of specific implementation details.
With Components, there are no local variables or properties to keep track of. You don't need to jump around between the places where views are created, constraints are added, and views are configured with a model. Everything is right there in front of you.
My advice to you: always prefer declarative style to imperative code. It's easier to read and maintain.
Composition over Inheritance
Here's a quick quiz: what does the code below do?
- (void)loadView {
self.view = [self newFeedView];
}
- (UIView *)newFeedView {
return [[FBFeedView alloc] init];
}
With inheritance, it could do anything. Maybe -newFeedView
was overridden in a subclass to return a completely different view. Maybe -loadView
was overridden to call a different method. In large codebases, proliferating subclasses make it difficult to read code and understand what it is actually doing.
4 Problems from inheritance cropped up often in News Feed before we used Components. For example, FBHorizontalScrollerView
had many subclasses that overrode different methods, making the superclass difficult to read or refactor.
Components are always composed, never subclassed. Think of them as little building blocks that can be plugged together to make something great.
But heavy use of composition results in deep hierarchies, and deep UIView
hierarchies slow scrolling to a crawl. So it's particularly handy that a component may specify that no view should be created for it at all.
5 In practice, most components don't need a view. Take the FBStackLayoutComponent
example from earlier; it stacks and flexes its children, but it doesn't need a UIView
in the hierarchy to perform this task.
Even though Feed's component hierarchy is dozens of layers deep, the resulting view hierarchy is only about three layers deep. We get all the benefits of using lots of composition but don't have to pay the cost.
If there's one lesson I learned from scaling a large codebase, it's this: avoid inheritance! Find ways to use composition or other patterns instead.
Automatic Recycling
A key part of using UITableView
is cell recycling: a small set of UITableViewCell
objects are reused to render each row as you scroll. This is key to blazing-fast scroll performance.
Unfortunately, it's really hard to get everything right when recycling complex cells in a codebase shared by many engineers. Before adopting Components, we once added a feature to fade out part of a story but forgot to reset alpha
upon recycling; other stories were randomly faded out too! In another case, forgetting to reset the hidden
property properly resulted in random missing or overlapping content.
With Components, you never need to worry about recycling; it's handled by the library. Instead of writing imperative code to correctly configure a recycled view that may be in any state, you declare the state you want the view to be in. The library figures out the minimal set of changes that need to be made to the view.
Optimize Once, Benefit Everywhere
Since all view manipulation is handled by Components code, we can speed up everything at once by optimizing a single algorithm. It's a lot more rewarding to optimize one place and see the results everywhere than to confront 400 subclasses of UIView
and think "This is going to be a big project…."
For example, we were able to add an optimization that ensured we don't call property setters (like -setText:
) when reconfiguring views unless the value has actually changed. This led to a boost in performance, even though most setters are efficient when the value hasn't changed. Another optimization ensured that we never reorder views (by calling -exchangeSubviewAtIndex:withSubviewAtIndex:
) unless absolutely necessary, since this operation is relatively expensive.
Best of all, these optimizations don't require anyone to change the way code is written. Instead of taking time to learn about expensive operations and how to avoid them, developers can focus on getting work done — a big organizational benefit.
The Challenge of Animation
No framework is a silver bullet. One challenging aspect of reactive UI frameworks is that animations can be more difficult to implement in comparison to traditional view frameworks.
A reactive approach to UI development encourages you to be explicit about transitioning between states. For example, imagine a UI that truncates text but allows the user to tap a button to expand inline and see all the text. This is easily modeled with two states: {Collapsed, Expanded}
.
But if you want to animate the expansion of the text, or let the user drag and drop to control exactly how much text is visible, it's not possible to represent the UI with only two states. There are hundreds of states corresponding to exactly how much text is visible at any point in the animation. The very fact that reactive frameworks force you to reason about state changes upfront is exactly what makes it difficult to model these animations.
We've developed two techniques to manage animations in Components:
-
Static animations can be expressed declaratively using an API called
animationsFromPreviousComponent:
. For example, a component may specify that it should be faded in when it appears for the first time. -
Dynamic animations are handled by providing an "escape hatch" back to traditional imperative and mutable code. You won't get the benefits of declarative code and explicit state management, but you'll have all the power of UIKit at your disposal.
Our hope is to develop powerful tools for expressing even dynamic animations in a simple and declarative way — we're just not there yet.
React Native
At Facebook, we recently announced React Native, a framework that uses the React Javascript library to manipulate UIView
hierarchies in native apps instead of DOM elements on web pages. It may surprise you to hear that the Components library I'm describing is not React Native, but a separate project.
Why the distinction? It's simple: React Native hadn't been invented yet when we rebuilt News Feed in Components. Everyone at Facebook is excited about the future of React Native, and it's already been used to power both Mobile Ads Manager and Groups.
As always, there are tradeoffs; for example, Components' choice of Objective-C++ means better type safety and performance, but React Native's use of Javascript allows live reload while running an app under development. These projects often share ideas to bring both forward.
AsyncDisplayKit
So what about AsyncDisplayKit, the UI framework developed to power Facebook's Paper? It adds the ability to perform measurement and rendering on background threads, freeing you from UIKit's main thread shackles.
Philosophically, AsyncDisplayKit is much closer to UIKit than to React. Unlike React, AsyncDisplayKit doesn't emphasize declarative syntax, composition, or immutability.
Like AsyncDisplayKit, Components performs all component creation and layout off the main thread. (This is easy because both our model objects and components themselves are completely immutable — no race conditions!)
AsyncDisplayKit enables complex gesture-driven animations, precisely the area that is a weak spot for Components. This makes the choice easy: If you're designing a complex gesture-driven UI, AsyncDisplayKit might be right for you. If your interface looks more like Facebook's News Feed, Components could be a good fit.
The Future of Components
The Components library has been adopted successfully in all feeds in the app (News Feed, Timeline, Groups, Events, Pages, Search, etc.) and is rapidly making its way to other parts of the Facebook app. Building UIs using simple, declarative, composable components is a joy.
You might think some of the ideas behind Components sound crazy. Give it five minutes as you think it over; you may have to challenge some assumptions, but these ideas have worked well for us and might benefit you too. If you want to learn more, watch this QCon talk, which explains some more detail behind Components. The Why React? blog post and the resources it links to are another great reference.
I'm excited to share the code behind Components with the community, and we're preparing to do so soon. If you have thoughts to share, I'd love to hear from you any time — especially if you have ideas about animations!
Postscript: ComponentKit was open sourced at F8. Check it out on Github.
-
Interface Builder makes Auto Layout easier, but since XIBs are impractical to merge, you can't use them with large teams. ↩
-
There is no shortage of articles and blog posts about debugging Auto Layout. ↩
-
We prototyped a very simplified version of News Feed that was powered by Auto Layout and it was challenging to get it to 60fps. ↩
-
objc.io has covered this topic before, and the Wikipedia article also does a good job of covering it. ↩
-
Similarly, in React, not every component results in the creation of a DOM element. ↩