Adopting SwiftUI In An Existing UIKit App

The Premise

Marc Laberge
SSENSE-TECH
5 min readSep 3, 2021

--

It all started when our team responsible for the SSENSE search experience wanted to include real-time auto-complete and suggestions as the user types out their search query. The dynamic nature of this desired interface, along with the vast number of desired reusable components, led us to trying out Apple’s SwiftUI in an existing UIKit app. This article outlines a few key learnings from the experience, such as interfacing with UIKit, cooperating SwiftUI’s MVVM architecture with MVC, and the very early nature of the SwiftUI technology.

SwiftUI

SwiftUI is a framework that utilizes declarative syntax, similar to React and Flutter, to quickly build reusable UI components across all of Apple’s platforms. This is a huge leap forward for iOS development compared to UIKit that aims to massively speed up development time. The catch is that it’s only available with iOS 13 and above.

With our app’s iOS 12 users rapidly decreasing, dropping iOS 12 was already on the horizon for us in order to use SwiftUI with iOS 13; so the timing was in place to make a start on integrating SwiftUI into our codebase. If the experiment went well, we would be in a great position to continue using SwiftUI in our app long term.

Although SwiftUI is much more production ready with iOS 14, we didn’t need any of the new SwiftUI features in iOS 14 for the new search: such as Grid and TextEditor. Despite the complex nature of the search view as a whole, the subviews themselves were just simple lists, so we were confident we could get the job done with a minimum deployment of just iOS 13.

The Integration

The jump to SwiftUI, also made for the perfect time to adopt an MVVM (Model-View-ViewModel) design pattern, instead of an MVC design pattern that was being used at the time. Whilst we couldn’t simply re-architect our entire app to MVVM, we were still able to integrate the new search into our existing app.

The example below shows how we wrapped our new SwiftUI SearchView in a UIHostingController in order to expose a UIViewController ready for use in our MVC UIKit app:

This approach also had the hidden advantage of being able to avoid using SwiftUI for navigation, which is one of SwiftUI’s weakest points, especially when it comes to custom navigation bars. As a result, we were able to continue using our own custom navigation bars with no extra effort, despite the entire contained view being 100% pure SwiftUI.

An important step of integrating a new technology is to document your findings and how it’s being used. We wanted to be well-informed about how we would be using SwiftUI from the very beginning. Whether it was regarding localized strings, or the naming convention of closures for button actions, maintaining documentation as we went along was a high priority. We also decided to include a set of recommended resources within the documentation, since the majority of the team was new to SwiftUI.

Testing

We were initially afraid that testing would be difficult, but the snapshot tests library we were already using proved to be great. If anything, our snapshot tests are even better now. The beauty of SwiftUI is the ability to very easily create modular and reusable view components which made writing snapshot tests a lot easier for us:

The toViewController() function is an extension we created for SwiftUI’s View protocol that returns a wrapped SwiftUI view in a UIViewController in order to be usable in snapshot tests. Since our views themselves aren’t taking in any dependencies, that responsibility now falls on the view models, and we were able to write many more concise and modular tests. The beauty of being able to easily support an MVVM architecture with SwiftUI, is that we could also independently test the functionality of our view models. These view model tests were again, much more concise and modular than many of our functional view controller tests that we currently have.

Testing view functions presented an additional challenge as they couldn’t be called externally. However, this was actually viewed as a positive, as it further encouraged us to isolate functionality into the view model as opposed to the view.

Pitfalls

Of course, using a newly released framework isn’t without its pitfalls, and we definitely ran into a few of these.

Although I mentioned earlier that having a minimum deployment target of iOS 13 as opposed to iOS 14 wasn’t a problem for us going forward, there were still very minor changes in the framework that we had to work around. One was that the Text view didn’t have a modifier for the text casing in iOS 13, which was problematic when using LocalizedStringKey. To pivot around this, we had to decide upfront what the letter casing of our raw strings would be, and include them like so.

Another instance of missing functionality in the iOS 13 iteration of SwiftUI, is the ability to programmatically set a TextField view as the first responder. SwiftUI’s TextField view is actually limited in many ways until you hit a deployment target of iOS 15, as we couldn’t even set the return type of the keyboard. Thankfully, SwiftUI is flexible enough to allow us to drop down to UIKit when necessary, using UIViewRepresentable:

In a way, we can think of UIViewRepresentable as the opposite of UIHostingController. Whilst dropping down to UIKit for a core component wasn’t ideal, it still allowed us to reap the rewards of SwiftUI at a higher level. It didn’t matter whether this sub-component was built with SwiftUI or UIKit under the hood, since that’s abstracted away from us when building higher-level components:

The final result of the newly added real-time auto-complete and suggestions in the SSENSE Mobile App search experience.
The final result of the newly added real-time auto-complete and suggestions in the SSENSE Mobile App search experience.

Moving Forward

Although we encountered some difficulties originally switching to SwiftUI, they all had reasonable solutions. Even with these pitfalls and workarounds, we were able to develop this new search feature incredibly quickly, thanks to the declarative nature of SwiftUI. It’s safe to say that our SwiftUI experiment was a proven success, and we’re now all very keen to use SwiftUI in our app going forward.

What makes things even more exciting, is that iOS 14 supports all of the same devices that iOS 13 does, so we’re optimistic to be able to drop support for iOS 13 sooner than we did for iOS 12. Not only would this mean being able to evade some of the workarounds we had to use, but it would also open up the possibility to create far more complex views with SwiftUI down the line, thanks to new SwiftUI views introduced in iOS 14, like Grid.

That being said, we are now at a crossroads in terms of deciding how we were going to go forward with SwiftUI in terms of architecture. Do we stick with MVVM? Do we build upon it and go in the direction of MVVM-C? Alternatively, do we take this as an opportunity to go all in with something new like Composable? This is something we’re still figuring out ourselves, as we’ve now started to experiment with Composable. Either way, the team is incredibly excited to embark on this new journey involving SwiftUI.

Editorial reviews by Deanna Chow, Liela Touré, and Mario Bittencourt.Want to work with us? Click here to see all open positions at SSENSE!

--

--