In June 2019, Apple made a huge announcement at WWDC, revealing a brand new way of building user interfaces called SwiftUI.
The promise of this new framework was not only an easier way to build user interfaces, but a way to do so across the entire Apple ecosystem. This would finally achieve the dream of using a single code base to build apps for the Mac, iOS, iPadOS, watchOS and tvOS.
iOS 13 has now arrived and developers can ship SwiftUI apps, after one of the worst beta cycles on record. I have been experimenting with SwiftUI on a pet project to get a feel for how ready it is for prime time.
The answer to that question is a solid, “it depends”.
One of the big selling points of SwiftUI is the clean syntax you use to describe your user interface. This declarative style decouples low level user interface elements such as UILabel or WKInterfaceLabel into abstracted, simplified constructs like Text.
There is actually a great deal of complexity hiding in this little snippet. If you have used Swift before, you are probably wondering, how on Earth does this compile?
The magic is all in Swift 5.1, which is the latest version of Swift that ships with Xcode 11. I highly recommend this article from Swift by Sundell, which explains the new features that power SwiftUI better than I ever could.
Cross Platform… Sort Of
SwiftUI works on all of Apple’s platforms, which gives developers the ability to write their user interface once and use it in every incarnation of their app. In practice though, this is limited by a few factors, some of which will improve over time and some that will never go away.
When building an app for the Mac, or an iPad, there is a very different set of user interface decisions to be made than when developing for an iPhone or the Apple Watch. Whilst sharing your UI seems like a great idea in theory, in practice, there are probably very few elements you can realistically share for anything but a very simple app.
Also, not every SwiftUI construct works on every platform, because not every platform is created equally. For example, here’s some simplified code from my one of my apps, which is targeting iPhone and watchOS.
These conditionals allow the compiler to only consider code within each block, based on the compilation target. This was necessary in order to share this code across platforms, as watchOS has no concept of navigation bars and list styles, meaning this code would not compile if used with watchOS.
This is ugly, but not totally unmanagable for a simple app. However, scaling this pattern to anything larger would quickly turn into a hot mess.
Observed Objects and State
One major problem that SwiftUI solves is keeping your data model in sync with your user interface. Anybody who has worked with Interface Builder knows that it can be a nightmare, involving various forms of callback hell, with a large blocks of code to achieve very simple outcomes.
Enter @ObservedObject, @EnvironmentObject and @State. These magical annotations allow you to connect your data model directly to UI elements, with changes to your data model triggering immediate updates to the user interface elements they are bound to.
SwiftUI offers three main ways to achieve this.
- @ObservedObject allows you to reference a custom data model that you maintain yourself somewhere in your application. This model must be passed into the constructor of any view that needs it.
- @EnvironmentObject allows a view to reference custom data models that have been injected anywhere in a view’s parent hierarchy, using the environmentObject() function available on any SwiftUI element. This removes the need for views to explicitly pass in data models through constructors, which could be many levels deep down a view hierarchy. I have personally found this to be more useful than @ObservedObject for most use cases.
- @State is like @ObservedObject, except SwiftUI will manage that object for you, so you don’t need to keep a reference to it anywhere except inside your UI code. This is useful for tracking transient interface state that you don’t need to persist between app launches.
Custom data models (like CoordinateData and LoggedInUser in the previous example) must implement ObservableObject, which tells SwiftUI that these objects have the ability to broadcast changes to themselves back to the UI. In order for individual fields inside these custom data model to broadcast when they change, one simply has to add the @Published annotation to the fields that should be watched.
This article does a great job of explaining the iterations Apple went through during the beta period to finally land on this succinct syntax.
Animation in SwiftUI is now for everybody, not just those who have a degree in advanced mathematics.
Any code example I’ve ever seen or used for animating iOS elements has been verbose, difficult to understand and buggy. SwiftUI hides a great deal of this complexity, and provides a number of simple operators that allow you to do quite powerful things with animations with only a few lines of code.
Here, increasing the scale of an Image will be animated on a linear curve, moving the scaling factor up by 1 each time the button is pushed, and animating through all values in between. This is achieved with a one-liner, animation(.linear).
Animations can also be expressed explicitly, in case you want to implement animation on a case by case basis, rather than baking it into your views.
This has the same effect as the previous example, but the animation is declared where the scale is incremented, rather than being an intrinsic behaviour of the Image being scaled.
A key advantage of animations in SwiftUI is the responsiveness of your UI while they are occurring. Because SwiftUI is taking care of animations for you, users can still interact with UI elements as they are animating, and even interrupt and reverse animations that are in progress, without blowing up your app.
There is a huge amount of detail in this topic, but suffice to say, this is one of the most game changing aspects of SwiftUI. Apple has a great introduction tutorial to get you started.
Backwards Compatibility with UIKit
SwiftUI does not yet support every element from UIKit, which will slowly improve over time. However, there is a fairly elegant solution to bridging UIKit elements with SwiftUI, called UIViewRepresentable.
A struct that implements this interface can tell SwiftUI how to create a custom view, and actions to perform when an update on that view is triggered.
Here’s an example of an activity indicator, which is handy as UIActivityIndicator has not yet been ported to SwiftUI.
This is great, but for one road block I came across in my app. I was hoping to use this fancy view in both my iPhone and watchOS app. However, UIActivityIndicator is not supported on watchOS, so even though SwiftUI lets me abstract away the underlying UIActivityIndicator view, that magic doesn’t extend to letting me use it in a place the underlying view isn’t supported.
You can also bridge your existing UIViewController code in a simliar fashion, allowing you to slowly move a UIKit app to SwiftUI without having to rebuild the whole thing at once.
Navigation and presentation of views in SwiftUI is a whole new ball game when compared to the old ViewController pattern. Declarative syntax means that there is no logical place to put code like ‘show an alert’ or ‘push view controller’.
For example, here’s a view that is constructed with an alert in place, which hides or shows itself based on the value of a bound @State variable. Showing/hiding the alert is achieved by flipping the boolean value, rather than directly interacting with the alert itself.
Similarly, presenting navigation views doesn’t have an explicit pushViewController() function anymore. Rather, this is all baked into a new construct called NavigationLink.
This all requires a big change in thinking, but once you’re used to it, you’ll never want to go back to the “old way”. Apple covers it nicely here.
One of the headline features of SwiftUI is the ability to live preview changes as you implement them. This has been the dream of many an iOS developer, as they sit and stare at Building 170 of 470 tasks for the 50th time to see if their one line change worked.
To be honest, there is not much to say about this feature, because it doesn’t feel quite ready. When it works, it is game changing, but these moments are few and far between.
Here are my favourite live preview errors, that still occur in the XCode 11 GM.
What did I do wrong? In this case it turns out I had neglected to set a value for one of my @EnvironmentObject fields, but nothing about this error pointed me in that direction.
Unfortunately, this is not the only way you can send live preview into a spin (literally). On occasion, live previews can take 20–30 seconds to load after certain kinds of code changes, while you watch a spinner in their place.
In addition, you will occasionally trigger the dreaded “Automatic preview updating paused”.
Having to hit a button to restart your live preview defeats the purpose of a live preview. And usually when things are in this state, resuming the live preview is far from instant, as it usually means a compile needs to be triggered and you’re back on the waiting train.
A Moving Target
It was evident from the beta period that SwiftUI is very young. Almost every beta release introduced breaking syntax changes and deprecations. This is not unheard of in betas, but the frequency and severity of these changes hints at this still being a moving target.
Whilst SwiftUI has clearly been in development for years, it is telling that none of the built-in Apple apps on iOS 13 are built using SwiftUI yet. I asked notorious Apple hacker Steve Troughton-Smith if he had anymore insight.
Apple frameworks tend to be at their best when Apple is dogfooding them, so until Apple fully commits to using SwiftUI in apps more complex than simple watchOS apps, things are likely to be a little dicey.
SwiftUI is also not backwards compatible with any operating systems prior to iOS 13 / watchOS 6 / macOS Catalina 10.15. So if you’re looking to use SwiftUI in your existing UIKit app, get ready to leave a whole bunch of your customers behind until they upgrade their operating system.
The developer tools have a great deal of catching up to do. Like the early days of Swift, things are very rough, but will hopefully improve over time.
Embrace the Future
Having said all that, SwiftUI is clearly the future Apple wants us to embrace.
If you have an existing UIKit app, I would not be in any great hurry until we see what SwiftUI 2.0 has to offer at WWDC 2020. It is worth considering how you might stage a slow transition to SwiftUI, one UIViewController at a time. But right now things are a little too unstable to bet the house on it if you already have an established code base.
If you’re starting a new app today, I would highly recommend giving it try, at least to get a feel for what you’re in for. Whilst you may lose some time battling with a framework in its infancy, a lot of that time is regained not battling with the various complexities of UIKit based development.
Ultimately SwiftUI isn’t going anywhere, so every iOS developer should be watching this space.
SwiftUI by Example
The SwiftUI bible, with a tutorial for just about anything you could ever think of doing.