A new library from Airbnb for declaratively building iOS apps Epoxy’s story on iOS began in 2016. Swift was still a very new language, and we had recently started to broadly adopt it across our codebase. With the new language, we were still experimenting with how to best build UI. Unfortunately, things weren’t going as well as […]
A new library from Airbnb for declaratively building iOS apps
Epoxy’s story on iOS began in 2016. Swift was still a very new language, and we had recently started to broadly adopt it across our codebase. With the new language, we were still experimenting with how to best build UI.
Unfortunately, things weren’t going as well as we had hoped — with our early paradigms for building UI in Swift we were not only experiencing hundreds of thousands of crashes in some of our core features, we also found that our engineers were deeply dissatisfied, giving their development experience a net promoter score of -78 on a scale of -100 to 100.
Why was this? One of the key sources of complexity was our API to update the content of a screen. Our in-house UI frameworks required engineers to write tedious and complex logic specifying every single index path with an update whenever content changed to get smooth update animations. And of course, this logic needed to change whenever product requirements changed — for example adding a new section to the home details screen that loaded its contents asynchronously after the page loads would require changing existing index path update logic to animate in that new section when it loaded.
Getting just a single one of these index paths wrong would lead to a crash in production. Surely there had to be a better way to manage this logic — but the real question was how — how could we rework this problem to prevent engineers from needing to maintain this logic by hand? And furthermore, how could we make our APIs for building UI a joy to use?
Stepping back, manually specifying index path updates in this way is a classic example of imperative programming. If you aren’t familiar with the concept, imperative programming is when you must describe exactly how the program should operate to accomplish your goal. Declarative programming, by contrast, allows you to describe your goal to the program, and have it figure out how it needs to operate to accomplish that goal.
In the case of our crashing updates, we were describing how the program should operate by specifying each index update manually, rather than describing our goal to the program, which is to update the screen’s content to a new set of content. This is the key insight that led to Epoxy. By moving up a level of abstraction — from imperative to declarative — we could both automate away this tedious work and make our app easier to build and maintain in the long run.
As such, to solve this problem holistically, Laura Skelton, the original author of Epoxy, reframed it into two parts — first, we needed to create a semantic API that allowed us to describe our goal to the program: the contents of any screen that we want it to render. And second, to automate the how, we needed to write a general solution to automatically calculate exactly which updates are required to apply the updated content whenever it changes.
To meet these goals, Laura created an API that allowed you to completely describe a UICollectionView’s content in a single data structure: an array of section models that contain section-level configuration, each containing an array of item models that contain item-level configuration, like how to construct a view, how to configure it when it’s reused, and so on. These models were designed to be cheap to create, so it was possible to create and apply the entire screen representation on every state change.
As part of this change, we also updated our views to be easily usable with a declarative paradigm. Rather than having many individually settable properties for each content property or many unique initializer parameters as we did before, we moved to a pattern of unified “content” and “style” types for each view that allowed you to specify all view properties at once. This gave us a truly semantic API where you can provide all properties necessary to construct or update a view when creating the content data structure without needing to provide closures with the logic to configure our view instances inline.
Once we had created the semantic content API, we needed a way to calculate the batch updates between any two of them so that we could ensure that we could define away our index path crashes entirely. We needed a diffing algorithm that was fast and general, and we turned to Paul Heckel’s fast O(N) diffing algorithm from 1978 to get the job done.
Paul Heckel’s algorithm was originally intended to operate on text in a document. We updated it to operate instead on view identifiers, known in the framework as “data IDs”. Furthermore, we added the concept of “updates” in addition to inserts, removals, and moves, allowing us to identify and apply content updates to existing view instances following a state change.
At the same time, our Android-focused teammates had also been working on a framework with many of the same guiding tenets as this new system — Epoxy for Android. While the implementation details and APIs differed due to platform differences, the systems were philosophically aligned. As such, we aligned our new system with the Epoxy for Android naming to simplify cross-platform collaboration and tooling.
Once we built this new semantic API and diffing algorithm atop UICollectionView and migrated our home detail page to it, we were thrilled to see that we still had our great update animations with none of the crashes and nor the maintenance overhead. We went from hundreds of thousands of index path crashes to zero, and a very negative developer experience to a +58 net promoter score. Furthermore, our fragile and complex imperative code was replaced with clear and intuitive declarative code. Since then, we have gradually migrated almost all of our screens to have their content driven by Epoxy collection views, and haven’t looked back.
What we didn’t quite realize at the time is that this approach would unlock an entirely new development paradigm for iOS feature development at Airbnb. As we looked through our codebase, we found there were still a large number of imperative UIKit APIs we relied on to build our app. Furthermore, we now found that there was a mental model mismatch between them — you’d build some parts of your features using declarative paradigms like the new Epoxy collection view, and other parts of it using imperative approaches: specifically for modal presentations, navigation stacks, and other views not contained in a collection view.
With this in mind, over the next few years we set out to update each and every one of these imperative use cases for driving the content of a screen to use a declarative API instead.
First, we started with the top and bottom bars on each screen. Almost every screen at Airbnb has one or more bar views affixed to its top and bottom. With our previous APIs for managing these bars, we needed to imperatively add, remove, and update bar views to the view hierarchy for every screen. To make these APIs declarative, we reworked the syntax in a similar way: we created a semantic API to describe the visible bars and applied updates to the view hierarchy using the same diffing algorithm as we use for Epoxy collection views.
This had many of the same benefits as we discovered with UICollectionView, making it easier and safer to add bar views to our screens.
Next, we turned to a more complex problem — managing the view controller hierarchy. This was one of the last pieces where we regularly required tedious imperative logic. Developers needed to imperatively push and pop view controllers from navigation stacks and imperatively present and dismiss view controllers.
To bring the view controller hierarchy into the declarative world, we adopted a similar pattern to bars views and collection view content to drive modal presentations and navigation stacks. We used the same diffing algorithm and models to specify the view controller hierarchy, just as we had specified the content on a page.
Now, all of these APIs could come together under a single umbrella of Epoxy as a complete system to completely describe a feature’s content using a declarative paradigm. With all of these systems together, developers can specify the entire content of any screen and drive its navigation using a uniform declarative approach. At this point, we decided that Epoxy was finally ready to be open sourced, and we did so in February. We invite you to take a look and try these APIs out for yourself.
With a declarative UI framework like Epoxy in place, we also found that unlocked an entirely new feature architecture. When you’re able to describe your app’s UI using a unified declarative API, it becomes trivial to adopt unidirectional data flow as a feature can render its UI as a function of its state. We’ve done so in our app and it has dramatically simplified state management and made it much easier to reason about a screen’s behavior, as the screen content can always be derived from a single source of truth: the state.
Finally, we’d be remiss if we didn’t mention SwiftUI, Apple’s new declarative UI framework for iOS. We’re incredibly excited about SwiftUI as it shares many of the same ideas as Epoxy, but we’ve found that it’s not currently a good fit for our use case. We’re looking forward to migrating to SwiftUI in the future, and expect to have a straightforward migration when the time is right as the Epoxy and SwiftUI APIs are philosophically aligned.
Epoxy wouldn’t be possible without the contributions of the original author, Laura Skelton, as well as its many contributors, including Bryn Bodayle, Bryan Keller, Tyler Hedrick, and many others.
To learn more about our mobile team and other related work, check out airbnb.io.
All trademarks are the properties of their respective owners. Any use of these are for identification purposes only and do not imply sponsorship or endorsement.