Unlocking SwiftUI at Airbnb

How Airbnb adopted SwiftUI in our iOS app

Bryn Bodayle
The Airbnb Tech Blog

--

Bryn Bodayle

When constructing an app’s user interface (UI), the choice of framework is incredibly important. The right UI framework can make an app feel smooth, responsive, even delightful, while a UI framework that doesn’t match an app’s needs can make it feel sluggish and broken. This principle extends to developer experience as well; a UI framework with well-designed APIs can enable engineers to express themselves fluently, efficiently, and correctly, while one with the wrong abstractions or inconsistent APIs can make engineers’ jobs more difficult by slowing them down with unnecessary complexity.

At Airbnb, we want our mobile apps to provide a world-class user experience and a world-class developer experience. This desire led us to build our own UI framework named Epoxy in 2016. Epoxy is a declarative UI framework, which means that engineers describe what their UI should be structured like for a given screen state and the framework then figures out how to make updates to the view hierarchy to render the screen contents. Epoxy uses UIKit under the hood to render views.

The iOS UI framework landscape shifted in 2019 with the introduction of SwiftUI, a first-party declarative UI framework that accomplishes many of the same goals as Epoxy. Although SwiftUI was not a good fit for our needs during its first three years, by 2022 it offered increased stability and API availability. It was around this time that we started to consider adopting SwiftUI at Airbnb.

In this post, we share why and how we ultimately replaced Epoxy and UIKit with SwiftUI at Airbnb. We’ll detail how we integrated SwiftUI into Airbnb’s design system, explain the results of this effort, and enumerate a few challenges we’re still working through. After reading this post you’ll understand why SwiftUI has met our high bar for both user and developer experience.

Evaluating and Planning for SwiftUI

Switching to a new UI framework is not a task that should be undertaken lightly. After much investigation, we posited that SwiftUI would not regress the user experience and would improve developer experience because of the following hypotheses:

  • Flexible and composable: SwiftUI would offer more powerful and flexible patterns to manage view variants and styling along with generic views and view modifiers. This should substantially reduce the number of views required to build the app, since it would be both easier to customize existing views and to compose new behavior inline at the callsite.
  • Fully declarative: SwiftUI code would be simpler to reason about and change over time. There should typically be no context switching between imperative and declarative coding paradigms like we had in Epoxy, for which engineers frequently needed to “drop down” into UIKit code.
  • Less code: As a result of SwiftUI being fully declarative, we believed it would take dramatically less code to build a SwiftUI view component. Generally, bug count correlates with lines of code.
  • Faster iteration: Xcode previews would enable near-instant iteration cycles on SwiftUI view components and screens, as compared to 30 second or more build and run iteration cycles with UIKit.
  • Idiomatic: SwiftUI would lower cognitive overhead when building UI, due to fewer custom paradigms and patterns. This would make it easier to onboard new engineers.

With these hypotheses in mind, we hatched a plan to evaluate and to adopt SwiftUI in three phases:

  • Phase 1: Build leaf views, such as reusable view components, from our design system
  • Phase 2: Build entire screens such as the reservation details page or the user profile page
  • Phase 3: Build complete features composed from multiple screens

As of the writing of this post, we have successfully completed the first two phases of SwiftUI adoption and for Phase Three await flexible navigation APIs to be added to SwiftUI. For the component (Phase One) and screen (Phase Two) phases, we conducted a small pilot in which engineers signed up to try SwiftUI for their use cases. The pilots were used to collect feedback and improve our SwiftUI support at that phase before progressing to the next. This approach enabled us to deliver value at each stage of adoption, as opposed to adopting SwiftUI for whole features from the get-go with a large and uncertain infrastructure investment upfront.

Enabling SwiftUI

We made a number of infrastructure and education investments to set engineers up for success.

Design System

First-class SwiftUI support for Airbnb’s design system was a key priority for accelerating SwiftUI adoption company-wide. Instead of merely bridging our existing UIKit components, we rebuilt the design system in SwiftUI to make it far more flexible and powerful.

Every view component in our design system supports styling to improve reusability via customization. We have a series of style protocols which, when combined with generated code, allow us to pass style objects down through the SwiftUI environment to mimic SwiftUI’s built in styling paradigms. One type of styling that conforms to this protocol is called “flexible styles”. Here’s some example code:

public protocol FlexibleSwiftUIViewStyle: DynamicProperty {
/// The content view type of this style, passed to `body()`.
associatedtype Content
/// The type of view representing the body.
associatedtype Body: View
/// Renders a view for this style.
@ViewBuilder
func body(content: Content) -> Body
}

This protocol allows us to create a style object with a collection of settable properties that can completely customize the rendering of a component. A content object is passed to the style so that it can access the view’s underlying state or interactions when creating a new view body. Here is an example style implementation for a numeric stepper (with some styling omitted for brevity):

public struct DefaultStepperStyle: DLSNumericStepperStyle {
public var valueLabel = TextStyle…


public func body(content: DLSNumericStepperStyleContent) -> some View {
HStack {
Button(action: content.onDecrement) { subtractIcon }
.disabled(content.atLowerBound)
Text(content.description)
.textStyle(valueLabel)
Button(action: content.onIncrement) { addIcon }
.disabled(content.atUpperBound)
}
}
}
Example stepper created from the default style properties

However, with flexible styles engineers can add an entirely custom stepper style with just a few dozen lines of code by implementing a new type that conforms to DLSNumericStepperStyle. That style can be set on a view using an autogenerated view modifier:

DLSNumericStepper(value: $value, in: 0...)
.dlsNumericStepperStyle(CustomStepperStyle())
Example stepper created from custom style properties.

Since optimized accessibility support is implemented in the DLSNumericStepper view, custom styles automatically get the appropriate accessibility behavior. We have used this flexible styling approach throughout the implementation of our design system, which allows product engineers to build new component variations quickly and without accessibility bugs.

Epoxy Bridging

Epoxy powers thousands of screens in the Airbnb app. To enable seamless adoption of SwiftUI, we built infrastructure to enable Epoxy not only to bridge SwiftUI views into UIKit-based Epoxy lists, but also to bridge Epoxy UIKit views to SwiftUI.

To bridge SwiftUI views to a UIKit Epoxy list, we created an itemModel view modifier that establishes the Epoxy identity for the SwiftUI View. In the implementation, this method wraps the view into a UIHostingController and embeds it within a collection view cell. This utility unlocked the first phase of our SwiftUI rollout by making it trivial to adopt SwiftUI in our existing Epoxy screens.

SwiftUIRow(
title: "Row \(id)",
subtitle: "Subtitle")
.itemModel(dataID: id)

Similarly, one can bridge UIKit views to SwiftUI with a view extension that creates a SwiftUI view from a UIKit component using its content, style invariants, and any additional view configuration. In the implementation, this API uses a generic UIViewRepresentable, which automatically creates and updates the UIView as its content and style change.

EpoxyRow.swiftUIView(
content: .init(title: "Row \(index)", subtitle: …),
style: .small)
.configure { context in
print("Configuring \(context.view)")
}
.onTapGesture {
print("Row \(index) tapped!")
}

Given the vastly different layout system of SwiftUI, properly laying out a UIKit component was a challenge. We developed a configurable approach that automatically supports complex views such as UILabel, which requires an additional layout pass to properly size.

Unidirectional Data Flow

With Epoxy we found that leveraging a unidirectional data flow pattern made our UI predictable and easy to reason about. We built our screens so that the Epoxy content is rendered as a function of the screen’s state. User interactions are dispatched as actions that result in mutations to the state, which trigger a re-render of the screen. We use a StateStore object to house screen state and handle actions to mutate that state. To adapt this pattern to SwiftUI, we updated our StateStore to conform to ObservableObject which allows the store to trigger a re-render of the screen’s SwiftUI View on state changes. We found that engineers preferred to continue to build screens in SwiftUI using this approach, since it enables the business and state mutation logic to be kept separate from the presentation logic. In many cases we were able to shift screen logic from Epoxy to SwiftUI screens with no changes. To illustrate the similarities, here is a simple counter screen implemented in both view systems:

// In Epoxy/UIKit:
struct CounterContentPresenter: StateStoreContentPresenter {
let store: StateStore<CounterState, CounterAction>


var content: UniListViewControllerContent {
.currentDLSStandardStyle()
.items {
BasicRow.itemModel(
dataID: ItemID.count,
content: .init(titleText: "Count \(state.count)"),
style: .standard)
.didSelect { _ in
store.handle(.increment)
}
}
}
}
// In SwiftUI
struct CounterScreen: View {
@ObservedObject
var store: StateStore<CounterState, CounterAction>


var body: some View {
DLSListScreen {
DLSRow(title: "Count \(store.state.count)")
.highlightEffectButton {
store.handle(.increment)
}
}
}
}

Testability

To ensure a high quality product, we wanted our SwiftUI code to be testable by design. Snapshot testing is our primary approach for testing views, so we use a static definition to provide named view variants both to our component browser and to our snapshot testing service:

enum DLSPrimaryButton_Definition: ViewDefinition, PreviewProvider {
static var contentVariants: ContentVariants {
DLSPrimaryButton(title: "Title") { … }
.named("Short text")


DLSPrimaryButton(title: "Title") { … }
.disabled(true)
.named("Disabled")
}
}

Since we’re returning view variants here, there is a lot of flexibility in what you can test–the framework accepts any content variation or combination of view modifiers. Additionally, we conform these definitions to SwiftUI’s PreviewProvider protocol and convert these content variants into the expected return type so that engineers can rapidly iterate on the component using Xcode Previews.

Unlike declarative UI frameworks on other platforms, SwiftUI does not provide a built-in testing library. In order to support behavioral-style tests of components and screens, we integrated the open source ViewInspector library, to which we’ve also contributed.

Education

We heard from some of our peer companies that a significant challenge in adopting SwiftUI was building in-house expertise across a large iOS team. To address this proactively, we held multiple half-week SwiftUI workshops focused on SwiftUI fundamentals, which nearly half of our iOS engineering team attended. Attendees reported that their confidence in SwiftUI fundamentals increased by 37%, and their confidence in building new components increased by 39%. Additionally, we found that attendees reported their SwiftUI expertise as 8% higher than those that did not attend a workshop nearly a year later.

Findings on SwiftUI

Lines of Code

Given Airbnb’s multimillion line iOS codebase, we were excited by the potential for SwiftUI to reduce the amount of code required to build UI. In an early experiment in which we rewrote our review card we saw a 6x reduction in lines of code –from 1,121 lines to a mere 174 lines of code! Over the past 2 years we have seen reductions in lines of code of similar magnitudes as our SwiftUI adoption has progressed.

Performance

UI performance was a key concern as we evaluated SwiftUI. Fortunately, after running multiple experiments, we verified that the page performance score when using SwiftUI was comparable to a UIKit implementation. We noticed a small overhead when instantiating UIHostingController, but were able to reduce this by adding a reuse pool of hosting controllers to Epoxy.

Adoption & Developer Satisfaction

With much excitement about SwiftUI within the company, organic adoption of the framework has been rapid. Our limited pilot of building components in SwiftUI began in January 2022, with general availability beginning later that May. Building entire screens in SwiftUI entered the pilot phase in October 2022 and then entered general availability in January 2023.

As of September, we have over 500 SwiftUI views and roughly 200 SwiftUI screens. Many of the screens for Airbnb’s 2023 Summer Release were fully powered by SwiftUI.

The growth of SwiftUI views and screens in Airbnb’s product.

Airbnb’s iOS engineers are also highly satisfied with SwiftUI. In our most recent survey, 77% of survey respondents said that SwiftUI improved their efficiency. Many respondents mentioned that their efficiency would improve further with more SwiftUI experience, including those that rated it as slowing them down. 100% of survey respondents said that SwiftUI did not negatively affect the quality of their features, and some cited SwiftUI as an improvement to their code quality.

Challenges

Though the move to SwiftUI has by and large been a major success, we have encountered the following challenges:

  • While Swift and its surrounding foundation have been open sourced, SwiftUI’s implementation remains a black box. If SwiftUI were open sourced, we could better understand the framework and debug more effectively.
  • Our visibility into the evolution of SwiftUI is limited to yearly announcements. If we had a clearer understanding of where SwiftUI is headed, we could better prioritize our adoption focus and know where to invest in custom solutions.
  • Airbnb supports the latest two iOS versions. If newer SwiftUI APIs were backported to older iOS versions, we could take advantage of powerful new features more quickly and spend less time writing fallback solutions.
  • In order to fully drop UIKit, we will need a set of SwiftUI APIs that support custom transitions and navigation patterns.
  • We’ve run into a number of challenges and limitations using LazyVStack and ScrollView, including:
    – Insertion, removal, and update animations are often broken.
    – Prefetching offscreen cells and prefetching images or data is not possible.
    – Some states are reset when scrolled offscreen.
  • The SwiftUI APIs for text input don’t support all the features which their UIKit counterparts supported, so engineers must bridge to UIKit.
  • We have 18 open feedbacks with Apple that document SwiftUI bugs or enhancements that we’ve come across.

Conclusion

In spite of these challenges, overall we have experienced smooth sailing in our careful adoption of SwiftUI at Airbnb. By rebuilding our design system, prioritizing education, and providing seamless integration with our existing frameworks, we have improved developer velocity and satisfaction while maintaining a high quality bar. We’re excited to watch SwiftUI continue to evolve and power more experiences in our app!

Thanks to Eric Horacek, Matthew Cheok, Michael Bachand, Rafael Assis, Ortal Yahdav, Nick Fox and many others for all of their contributions to SwiftUI at Airbnb.

All product names, logos, and brands are property of their respective owners. All company, product and service names used in this website are for identification purposes only. Use of these names, logos, and brands does not imply endorsement.

--

--