Transitioning to SwiftUI with Server Driven UI in the HEMA app
How SwiftUI became the catalyst for efficiency and developer happiness
Five years ago, in 2019, Apple introduced SwiftUI as a new UI framework. It was revolutionary because it allowed for declarative UI construction instead of the imperative approach used in UIKit. At Q42, we quickly began experimenting with it, as detailed in this earlier blog post. Initially, SwiftUI was strong in basic functionalities and easy to use for small apps. However, for larger e-commerce apps like HEMA, SwiftUI wasn't mature enough, lacking many features and often requiring fallback to UIKit. But with the release of iOS 15 and especially iOS 16, a turning point was reached. These versions introduced features like pull-to-refresh and improved navigation. Even Apple started building more of its apps in SwiftUI. At our HEMA team, we realized it was time to consider the transition. In this blog post, I'll take you through our journey and how you can migrate to SwiftUI yourself.
Tech stack
At Q42, we've been building the app for Dutch variety store-chain HEMA since 2019. We've gradually rebuilt the old app, replacing web screens with native ones along the way. Just before we started working on the HEMA app, we delivered Primephonic (now Apple Music Classical), leveraging a new paradigm in building internet-connected apps: Server Driven UI (read more in our earlier blog post or check out these presentations). When HEMA came along, we found that this approach was also a great fit for them. By moving the responsibility of what is shown on screen to the backend, we could deliver features faster. The screen is built using standalone components, which will help us during this transition.
Wrapping SwiftUI in a UIKit environment
As mentioned, the HEMA app is built using SDUI. The SDUI part is split into navigation and UI rendering. For the transition we mainly focused on the UI rendering part. The navigation part we still leave based on UIKit UINavigationController
and will be transitioned in the future. When rendering the UI components, we leverage a UICollectionView
with a compositional layout (learn more). This layout framework is designed to combine small components into a full layout. Apple already introduced the UIHostingController
in iOS 13, but it did not allow us to interact with CollectionViewCells
. This came with iOS 16 where the UIHostingConfiguration
was introduced (learn more). This setup was exactly what we needed for the transition, but we had one issue: HEMA still wanted to support iOS 15 users. We found a solution in the UIHostingConfigurationBackport
(learn more), which supports iOS 15 as well. In the code block below we see how you can use this backport in your iOS 15 codebase.
Below you see the code to be used with iOS 16+.
Technically, we could now start the transition. But before building, we wanted to test performance. We converted one component to SwiftUI and added it to a screen a thousand times. We tried scrolling through the list, and it all seemed to work as expected. Time to start the transition! 🚀
Just testing one component validated that we can technically make this work. But doing this over the whole codebase is something else. We needed to do the transition step by step, converting one component at a time. We needed to come up with a way that both UIKit and SwiftUI components would be supported during the transition. To do this we needed some form of switch to detect if the component was already transformed to SwiftUI. In our existing setup we already have a place where we transform from the component domain models to UIKit views based on a protocol extension. For components where we migrated to SwiftUI we added a new protocol called SwiftUIContentViewProviding
. When this protocol was detected we wrapped the SwiftUI view in a UIHostingConfigurationBackport
. In the code block below you can see how this switch was made.
if let component = component as? SwiftUIContentViewProviding {
configureSwiftUICell(cell: cell, component: component)
} else {
guard let cell = cell as? ComponentCell else { continue }
configureUIKitCell(cell, component: component)
}
Aligning roadmaps
These transitions are not without impact and resource requirements. The iOS team was convinced that this migration would simplify our view logic significantly and make the codebase more accessible for new iOS developers. But our technical explorations were initially met with resistance from the business side. Convincing the HEMA product owner and non-technical stakeholders was challenging because the real cost-benefit was not immediately clear—it was more of a gut feeling. We aligned the transition of the new SwiftUI views with the rework of the new Visual Identity. This ensured that we could work on both the transition and the client's roadmap. Initially, progress was slow, but once all the building blocks were in place, we accelerated significantly. This was the tipping point for convincing the business, as illustrated by this quote from our product owner:
Haha I’m confused! How did you do this so quickly? 🚀
Within 60 minutes, we created a ticket, estimated it, built it, reviewed it and released it in an internal build. He just couldn’t believe we had the development speed to go this fast. This moment captured also what we needed to convince the rest of the stakeholders. In the end pushing out new visual components faster than the backend team could keep up with. SwiftUI is a fantastic framework that really lets you focus on the exciting stuff.
Screen layouting
As the component transitions were underway we started to look into how to transition from CompositionLayout to SwiftUI as well. Compositional Layout is an extremely powerful layouting tool, but can be hard to grasp for new project members. Using the tools provided by SwiftUI, we were able to recreate the required layout with just the VStack
, LazyVStack
, HStack
and LazyVGrid
building blocks. On acceptance, we ran against this new layout view based on a feature flag. However, just using these building blocks was not enough and required some tweaking to achieve the right HEMA feel.
Design collaboration
With the introduction of SwiftUI, you can preview your work in the Xcode canvas. This enabled faster development and increased collaboration with the design team. When starting this transition, we had no good overview of the components we had. With the transition, we gained a direct view of how a component looked, and we worked with the design team to create a new component library. After the transition, we implemented a weekly design meeting to address small design issues or requests. This allowed both teams to focus on building new and exciting features and creating an even greater app. For us, this meant working on the new layout view.
Caveats
Describing the transition sounds simple, but we did run into three issues.
- State Management: we cannot store data inside a view with the
@State
property. When a cell is dequeued, the state is lost (read more). When we ran into this issue, we refactored the view to be stateless first and transitioned after. - Height Changes: when a component changes height, it is not respected when using the
UIHostingConfiguration
. An example of this is the DisclosureGroup (read more) that expands when clicked. When components broke the layout, they were postponed until a later stage. During Christmas, we had a release freeze, and most colleagues were off. I decided to rip off the bandaid and do the final touches of the new layout code and remaining components. We had been running all SwiftUI code in our internal test builds for a long time and were confident with a big bang release. - ScrollView issues: as the release date approached, we started to run the final code against production and noticed that the ScrollView was hanging sometimes. This issue was caused by how we nested the SwiftUI building blocks. Don’t wrap a
LazyVGrid
inside aLazyVStack
. 🙈
Conclusion
In the end, we're really happy with the transition to SwiftUI. It boosted efficiency, developer happiness, and design collaboration. We've now completed this transition and are excited to look forward to building navigation in SwiftUI with the new NavigationStack
in iOS 16. I wish everyone good luck with their SwiftUI transition and happy coding.
For the geeks, here are some statistics on the work we have done during our transitioning period:
- 58 SDUI components converted from UIKit to SwiftUI
- 4 developers worked off and on this transition
- It took 442 days from proof of concept to full release
- wrote more than 10.000 lines of code
Do you also love working with new app technologies like SDUI? Check our job vacancies (in Dutch) at werkenbij.q42.nl!