SwiftUI: Is it ready yet?

SwiftUI was introduced almost 3 years ago at WWDC 2019 as a simple, declarative way to build user interfaces across all Apple platforms. As with the announcement of Swift at WWDC 2014, it was clear from the audience and online community reaction that this was a significant milestone. Though as was also the case with Swift, it was something many in the developer community had been awaiting for some time.

Comparing SwiftUI to UIKit is really about comparing different programming paradigms. Declarative programming is about describing what should happen, where as imperative programming (UIKit) is about describing how it should happen. An imperative UIKit approach in practice generally involves mutating state, then applying that mutation to a (usually pre-existing) view. The responsibility of ensuring the view is consistent with the current application state is on the programmer. Failing to do this is one of the most common causes of bugs, as it involves ensuring every code path that leads to a state transformation ultimately leads to a UI update. In contrast, a declarative SwiftUI approach in practice involves describing what a view should look like for a given state, then mutating the state bound to that view - the responsibility of reacting to state changes is on the system, thus eliminating a whole category of state/UI synchronisation bugs.

This property of a SwiftUI view reacting to state changes automatically is what also allows it to be described as reactive. Reactive programming is nothing new within the apple ecosystem, third-party frameworks like ReactiveSwift and RxSwift have been used by teams for years. But it’s no coincidence that Apple introduced its own solution alongside SwiftUI: Combine. For some time now, MVVM has been one of the most popular application architectures within iOS. The central principle of this pattern is data binding, which is a technique of connecting a data provider (a view model - VM) to a consumer (a view - V), and keeping them synchronised. While Combine is agnostic to the view layer, there are benefits to using it alongside SwiftUI, none more so than the implicit updating of a view that is bound to a view model whose state changes. This is achieved by making a view model observable (@ObservableObject), and referring to it in the view as observed (@ObservedObject/@StateObject). With this simple step, any time the state of the view model changes, the view will be re-evaluated. Again, we can still make use of Combine as the data binding mechanism in UIKit, but it involves the additional step of subscribing to emissions from a view model in the view layer (the controller), and explicitly updating all relevant view properties.

The guiding principle of a declarative UI framework is that all views can be modelled as a function of some state: F (S) = V. This makes views disposable, light-weight, and most importantly, stateless.  In UIKit, a view continues to exist until we navigate away from it. When we mutate the state represented in a view, we must then change the visible values on that same view. In SwiftUI, every time we change the state of a view, the system is actually creating a whole new view. These views are implemented as structs, with only a single protocol requirement: a computed body. This is what the system invokes when it determines that a view needs to be re-evaluated. The result of this body is really what constitutes the visible view, and in theory there’s no reason a pure function (or closure) could not fulfil the same role, which is what allows SwiftUI to also be described as functional (don’t worry, we’re not talking monads or lambdas!) In UIKit a view is a stateful object, meaning its state exists in multiple places - the view itself and the model that it is backed by. This takes us back to the view/model de-synchronisation problem that SwiftUI (and other declarative frameworks) solve. In SwiftUI, sources of truth are a core concept, and the golden rule is that there should only ever be one.

This paradigm shift from imperative stateful UI to declarative-functional-reactive UI is one that is occurring across the entire mobile space. The shift can be traced back to React Native in 2013, which started as a Facebook hackathon project, and more recently to Flutter, which started as a fork of Chrome. While these cross-platform solutions have gained significant traction, many teams preferred to work with the native tools of their platform vendors. The native vs cross-platform question is beyond the scope of this post, but there are a variety of reasons teams prefer native solutions, not least being the difficulty in porting certain capabilities - in iOS 15 for example Apple released seven Swift-native frameworks, which would require significant engineering effort to be ported to Dart, JavaScript, or other languages, and there will always be a delay from when new features arrive in an OS to when they are ported. Although Flutter is a Google project, it is not part of the Android project, so it’s understandable why Android developers are cautious of adopting it (especially when browsing through the list of projects killed by google). It’s this that led Google to introduce their own first-party declarative UI framework, Jetpack Compose, also in 2019, which Google engineering manager Clara Bayarri hailed as the “future for UI on Android.” Native or cross-platform then, the future certainly looks declarative.

So re-framing the question of SwiftUI vs UIKit, we might instead ask, why not SwiftUI? As with any new framework, it often takes time to achieve parity with existing solutions - both in terms of features and adoption. So regardless of the methodological benefits, for most teams the question remains ‘is it ready yet?’

It’s important to note that SwiftUI is in fact built on top of UIKit. With each release, Apple’s development teams have translated more UIKit capabilities to SwiftUI, but UIKit has been around for almost 16 years, so it goes without saying that SwiftUI does not have the full API coverage that UIKit does. It’s here we realise the parallels to the Objective-C to Swift transition break down: Objective-C could be bridged to Swift on day one, meaning all Objective-C frameworks were immediately available to Swift. Apple’s engineers appear to be taking a cautious approach to porting UIKit capabilities to SwiftUI, ensuring the capabilities that make the cut remain stable for years to come. However this does mean that we do occasionally run into requirements that do not have a native solution in SwiftUI. Usually there is a workaround, though sometimes these are not trivial, and require time that would not be needed with UIKit. Certain capabilities are simply not supported in SwiftUI, for example there is no native web view. It’s for cases like this that any UIKit view can be wrapped in a SwiftUI layer (UIViewRepresentable) and treated as a native SwiftUI view by other views.

In terms of adoption, since SwiftUI was only introduced in 2019, it is only available in iOS 13 and later. Most teams have a business requirement to support users on previous OS versions, usually anywhere from 1-3 years back. For this reason alone it has taken pre-existing projects time before they can even consider the question of adoption. As we now approach the 3 year point of iOS 16 we can expect this to accelerate. Beyond pure OS availability it is likely that many teams have held off due to how fundamental a change in approach SwiftUI is - far beyond simply swapping out the view layer, there will be implications across all layers of the app architecture. Teams in this situation can adopt gradually since SwiftUI views can be hosted by UIKit views, allowing developers to build skills over time, and preventing the need to rewrite the entire app all at once. For new projects, the reasons to stick with UIKit reduce each year, whilst the benefits of rapid development with less boilerplate and potential for bugs make diving in more and more appealing.

It is likely that SwiftUI is the future of UI on all Apple platforms, and for most teams, it probably can be the present, whilst UIKit (and AppKit on macOS) is the past, and for some teams, may still need to be the present… for a bit longer at least!

Previous
Previous

Architecture in SwiftUI