Thoughts on SwiftUI
I am trying to wrap my head around essential SwiftUI concepts. This is where I am today. Something I wrote here may be wrong but that’s ok, it’s a learning process. Do tell me if I’m wrong on anything here.
SwiftUI is a meteor blast.
There is no two ways around it — it’s a tectonic shift which disrupts all existing Apple’s UI frameworks: UIKit, AppKit, WatchKit. It even unofficially deprecated the brand new Catalyst initiative to bring iPad apps to Mac.
Apple by-passed Swift’s regular development process by pushing through several Swift-Evolution proposals, some of which were not even known outside Apple until WWDC19 keynote. Without those changes, SwiftUI would not be possible in its current form.
It’s conceptually so different that every single iOS developer out there is reduced back to the beginner status. Knowledge of UIKit will come in handy in the next few years and be golden in the years beyond, as there will be so much contracting work available to maintain and fix millions of existing UIKit codebases. Experience in building good code structure and architecture never goes away. But – we are all juniors again in the SwiftUI domain.
It’s seriously funny to read reactions to SwiftUI. Most developers (me included) were kicking WatchKit UI down into the mud all these years and Apple responded by expanding and extending those ideas – stacks, groups, auto-sizing to content etc – into SwiftUI. Apple can be stubborn and funny like that.
The future is set even if it may realistically start in a year or two at best, because SwiftUI is system framework and thus tied to iOS/iPadOS/tvOS 13, watchOS 6 and macOS Catalina (10.15).
The future is set but present is not going anywhere. I really don’t envy the monumental task in front of anyone trying to get into iOS dev world in next 12-18 months. They’ll need to concurrently learn two vastly different UI approaches, which is as hard as it sounds.
SwiftUI’s View
is lightweight and practically free to create. It’s infinitely stackable, tile-able, embeddable, you name it. It has 0! properties. It inherits nothing, it’s actually a protocol
which can be adopted by any type you create. All Apple examples and Xcode templates use structs but the fact View
is protocol opens up some interesting avenues for experimentation.
Every Apple speaker made sure to highlight the weightlessness of SwiftUI views. Because there is no such thing as view reusability, one funny test would be to drop a List
/ForEach
of 10k or 1M mildly complex cells and scroll through it. Will it be smooth? Will it hit memory constraints? That was the major issue with all the 3rd party declarative attempts based on UIKit (longtime readers would know what I think about those).
SwiftUI controls are semantically singular but visually distinct across platforms. As example, there’s just one Toggle
control now, automatically adapted by platform’s runtime to appropriate look and feel.
Every platform’s HIG is applied by the SwiftUI runtime, with no input from developer required. You can adjust properties of any (sub)view as you see fit by calling pre-defined methods (called modifiers) on instances of views and controls. There is no way I found to globally customize margins, paddings, fonts, colors etc. However, the nature of how SwiftUI works lends itself to relatively quickly build custom Text()
views like Title()
, BarText()
, CellText()
etc.
No ViewControllers. No Controllers of any kind, just views. Navigation is inter-leaved with content (booo!) like you used to do in storyboards with segues. Speaking of – there’s no such thing as .xib
, .storyboard
or anything like it. All layout is done in code.
You control neither the layout or drawing, at all. SwiftUI is declarative layout, where you describe what you want the layout to be and SwiftUI runtime will decide when and how to (re)draw that description into actual display on the screen.
This is such a shift from imperative world we had so far, it deserves more thorough look.
In UIKit, you change view’s background color by simply doing:
someView.backgroundColor = .red
Or you could have changed some AutoLayout constraints to alter its size and/or position. You could do this because you always had a way to reference any view, anywhere in the subview hierarchy.
Even with that, none of those changes would render immediately. You have to wait until the next runtime’s layout / drawing pass although you have the ability to nudge the runtime to make that pass using layoutIfNeeded()
, among other things.
There is no true equivalent for that in SwiftUI. View
-conforming constructs describe the layout and content and SwiftUI runtime will decide when and how to turn that into actual display on the screen. There is no facility to request immediate layout and drawing on a whim.
More important change is that what you see on screen is a derivative of layout description, optionally based on some model data. That model is not really inside your views even though they are accessing it. Confused yet?
Imagine this simple custom view component, basically a label displaying given string:
struct Greeting: some View {
var name: String
var body: some View {
Text(name)
}
}
And then you use that Greeting
view inside larger Profile
view.
struct Profile: some View {
var user: User
var body: some View {
Greeting(name: user.name)
}
}
You may think of name
as local model for the Greeting
view and user
as local model for the Profile
view.
Now let’s imagine that something in your app changes the value of the user
property – how can you now update the Greeting? This is not possible:
var user: User {
didSet {
greeting?.name = user.name
}
}
because Greeting
instance does not have any sort of variable to refer it by. There is no greeting
property, no subviews
collection, because Greeting
view instance may not even exist under the hood; it could be just a bitmap text drawn in the Core Graphics canvas. SwiftUI can optimize the drawing of entire Profile
component in any way its developers deem proper. The underlying rendering can change between versions and OS updates. You don’t think about that since you don’t manage the display.
UIKit was built on top of MVC as locally relevant pattern in the context of particular screen or some subview. Your controller (where I don’t mean UIViewController
) has references to model objects and references to corresponding views. You write the code to coordinate between the changes in model data and rendering of those changes in the views.
SwiftUI is instead built on something completely different:
- you describe the intent to use some of the app’s model data
- you describe a view hierarchy where you use those model data
- runtime will maintain its own “controller” mechanism and each time you change the model data it will automatically redraw the view hierarchy as you described.
For developers, this is way, way simpler approach than UIKit. The only thing you think about, the only stuff you need to care about is managing the app’s model data. Nothing else. As explained above, C
and V
from the MVC
are now outside of app developer’s domain and you’re left with just M
. Sounds wonderful but how can that possibly work as (some kind of) copies of the model data exist in the views?
Well, they don’t. That’s what I meant when I said that you describe the intent to use model data, because they are not really copied into each view that access them. Each model object that you access in the views is accessed through reference semantics. All of them, it does not matter if they are value or reference types. You’ll understand why when you learn how @State
, @ObjectBinding
and friends actually work. In most simplistic terms I can fathom – SwiftUI is using global singleton model storage (Environment
) for all views in your app. Additionally, each view hierarchy may have its own ad-hoc singleton context as data model storage (for @State
, @ObjectBinding
).
It’s rather crazy to see this after years of “singleton are baaaaad” talks and best practices. Most surprising to me was to see this reversal promoted among the proponents of functional programming and declarative layouts.
I was never a big opponent to singleton’s usage in apps as some other devs; they have their place and purpose. But I very much dislike usage of singletons as magic floating hat offering what you need anywhere and at anytime you need it. That leads to messy pathways around the code which quickly grow out of control in a larger app. Incredibly, this is exactly what SwiftUI is offering.
It does not matter that you don’t create those singletons and they are managed and maintained by Apple’s dev team. The concept is what it is and I truly fear that inexperienced developers will just throw all their app’s data into Environment
and don’t think twice about it.
Last bit I want to touch on is the syntax itself. As Nataliya succinctly quipped, Swift 5.1 seems to have thrown all the progress up to 5.0 on its head.
The only true issue I have is with the function builders, the very thing that makes SwiftUI syntax possible. When you look at typical code:
struct LandmarkList: View {
@State var showFavoritesOnly = false
var body: some View {
NavigationView {
List(landmarkData) { landmark in
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
it’s really hard to distinguish between language constructs and data types, especially if you are new to Swift. This syntax is anything but glanceable and requires significant mental effort to scan through. This was never an issue before and I doubt it will become much easier over time. You’ll need to memorize SwiftUI constructs and there is no escaping that.
I was teaching Swift for few years and I know first hand that closures were, especially trailing closures, one of the hardest challenges for newbies to overcome. Function types are very abstract concept to internalize. Thus closures as unnamed function are just really weird to recognize but the good thing was that you could replace them with regular functions until you became more comfortable.
Function builders are an order of magnitude harder challenge than closures and I don’t see how you can replace them with anything. I mean, you maybe/probably can if you want to but then that’s not SwiftUI code anymore.
As a big proponent of leaning on platform and framework strengths, I’ll certainly go along with SwiftUI in future apps.
I may not like the framework concepts but it’s just waste of time to re-invent the wheel and use system frameworks in ways they were not designed for. It’s primary reason why I never cared much about RxSwift & co. in UIKit apps.
Good luck to us all.