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.