One of the very few questions that’s consistent across the board is “Should I use SwiftUI?”. The answer is simple: Not until version 2.0. The end, thank you for reading this article, thanks for all the fish. I’ll try not to end my life falling from space as a whale. There are reasons for this, though.
Tell me more about SwiftUI…
As a starter, the framework got announced in June 2019, during WWDC. And it took the world by storm. Finally something readily made for Swift, where the solid foundations are not Objective-C. Finally using our awesome modern language to create declarative interfaces.
Obviously, we didn’t need to ask Nostradamus to divine the reasons for this language. The main issue is a major divide between macOS and iOS, and a divide that’s getting bigger with the new ipadOS, watchOS and tvOS. In other words, there’s the beginning of an entire ecosystem, where most of the new hot stuff is based on iOS, some are odd balls (such as watchOS) and you have the good old macOS that’s getting yet again left out of the equation. You only have to look at the state of the different App Stores to figure out there’s an issue making people develop for Mac.
So we got an awesome new language (September 2014 for the first public app) that’s getting mature; people are only building stuff for iOS; people prefer to use Objective-C as the language is perfectly suited to the current UI API.
What would happen if we’d link all of this together? I mean, people would write a single interface, then we’d catalyze (macCatalyst) developer’s hard work into macOS without requiring any changes. Maybe one day on the Watch and TV? Maybe in the future, people could only purchase anything once and it’d migrate everywhere automatically (universal purchase)?
A task force got created, the project got pushed, a deadline got decided, and everyone worked to get there. Fast-forward today, we’ve been able to ship product with SwiftUI for the past few months.
So… Why should I use SwiftUI?
Because this is clearly the future. The team worked hard on all the latest tendencies for new applications, made sure to apply the greatest fads of the programming world, and all the buzzwords. The API is clearly made for Swift and no other language, and the official word was clearly to make it run on iOS perfectly, and allow for future extensions on all the other platforms, from the humble Watch to the powerful Mac. They sprinkled a little bit of ReactJS interfaces, they added where Swift 5 was going with publishers and observers, promises and futures. They added multiple ways to bubble up and down parameters. They made a declarative interface totally built at compile-time, with optimizations galore.
It’s really simple. And all the checkmarks are there. Build something? No Obj-C incantations, you merely say what you want to do, and here you go. As an example, this shows up our logo, a text, and an URL you can tap to open.
var misoservices_logo: some View {
VStack {
Image("about.misoservices")
.renderingMode(.original)
.resizable().aspectRatio(contentMode: .fit)
.frame(width: 300, height: 60)
.padding(EdgeInsets(top: 20, leading: 0, bottom: 10, trailing: 0))
Text(LocalizedStringKey("about.creator")).font(.headline).padding(10)
Text("https://misoservices.com")
.foregroundColor(.blue)
}.onTapGesture {
if let url = URL(string: "https://misoservices.com") {
UIApplication.shared.open(url)
}
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
}
Code language: Swift (swift)
And I’m being really frugal here, because to be honest, there are a million good reasons to actually use it, and they are all better than the last one. I enjoy my ride with SwiftUI, and its schemes are plain brilliant.
But you are not here for me to tell you what’s good, don’t you?
Why not using it, then?
No complex framework is ever ready in v1.0
Never. With Swift 5, we are starting to get a valid language, most bugs are fished out and fixed. We are getting stability and the important features. That’s nearly 10 years worth of development to get there, and tens of thousand of people working the language daily, in small and large projects.
So, to tell SwiftUI is still a rookie and wet under the ears is an understatement. We barely have the minimals here, and they are not consistent.
Remember the .NET debacle. Remember C#. Remember the first version of Swift. If you are an old geezer, remember the first version of Javascript. SwiftUI patently cannot be different.
They were rushed and lacked time
You need to ship this out one day. And they needed the language for the next features in 2019 and 2020. So it really shows the developers got pushed to cram everything needed in there. We were there when they decided to migrate from an objectDidSend
to an objectWillSend
model for all the events.
Not all the features are there. I mean major things such as the Activity Indicator needs to be bridged by the developer. Things such as the blur with vibrancy is not available either. So we cannot have an application as polished as we could with UIKit.
They have the “made by us” mentality
Apple is known for not caring about the rest of the world, and pushing their own agendas. They need something, they do something, even if they have to rewrite everything. It’s usually good, and with good intentions, though. Where I draw the line is when they have two internal projects, Swift and SwiftUI, and the SwiftUI team clearly decided to go with things that weren’t really made for Swift.
You read that right! The SwiftUI framework has one sole purpose: to be usable in Swift. But everything that Swift offers good, SwiftUI broke. And that even includes the new Combine framework. I make bold claims. Let’s prove them.
For starter, simple things. Swift imposes security. The entire goal of Swift is to be secure, to the smallest of details. No more pointers to null! And if you do something sketchy, you are being warned numerous times. A very simple example is the enumeration class. You need to provide a switch that has all the cases:
var descriptionKey: LocalizedStringKey {
switch self {
case .system(let error):
return LocalizedStringKey(stringLiteral: error.localizedDescription)
case .localizedSystem(let error):
return LocalizedStringKey(stringLiteral: error.localizedDescription)
case .photoLibraryAuth(let error):
return error.descriptionKey
}
}
var tableName: String? {
switch self {
case .system: fallthrough
case .localizedSystem:
return "ModelLibrary"
case .photoLibraryAuth(let error):
return error.tableName
}
}
Code language: Swift (swift)
But in the case of a SwiftUI View, you cannot use the switch-case paradigm, you need to if-else everything!
func button(_ group: PaperSize.Standard, _ paperSize: PaperSize, custom: CustomPaperSize? = nil) -> some View {
HStack {
if paperSize.isImportant {
Text(paperSize.localizedStringKey, tableName: "PaperSize").bold()
} else if paperSize.isBasic {
Text(paperSize.localizedStringKey, tableName: "PaperSize").bold()
} else {
Text(paperSize.localizedStringKey, tableName: "PaperSize")
}
Code language: Swift (swift)
That’s a very simple example, but it prevails everywhere. Even more… Swift created Float
and Double
. But SwiftUI still uses that pesky CGFloat
for no reason whatsoever. It is mind-boggling how something supposed to be made for Swift specifically actually foregoes its most basic object types!
Your SwiftUI application is unstable by design
Let’s take a small piece of code. Let’s pretend I want to get the initial bounding box of something. Normally, in a normal framework, I would have a var initialFrame
in the class definition and a self.initialFrame = self.frame.bounds
in some initializer, which would pick up the frame, as initially set. The end.
But you cannot do that here. It turns out you do not know your own coordinates. So you must use a special reader for your geometry. And you need objects in your GeometryReader
. So you create a Color.clear
in it, and tell it when it appears, to set the frame.
It also turns out your struct is actually immutable. So you must use a trick, a @State
, to tell your variable is residing elsewhere that’s mutable.
(For completeness, using Spacer()
as an example – code is more complex but it would only clutter)
@State var initialFrame = CGRect()
var body: some View {
Spacer().overlay(GeometryReader { geometry in
Color.clear.onAppear {
self.initialFrame = geometry.frame(in: .global)
}
})
}
Code language: Swift (swift)
This is simple. No biggie (still took me a lot of time to figure that out). But this will not work.
So you get told at runtime you need to dispatch this at a later moment. This means setting it out in the main thread, asynchronously.
@State var initialFrame = CGRect()
var body: some View {
Spacer().overlay(GeometryReader { geometry in
Color.clear.onAppear {
DispatchQueue.main.async {
self.initialFrame = geometry.frame(in: .global)
}
}
})
}
Code language: Swift (swift)
And this, my friends, actually works. Yay! Simple, efficient! Well, no, not really. And this is where I get the first real instability. You are telling your static object that resides in your stack you wish to update at a later time. What happens if your object appears and suddenly disappears for any given reason? And believe me, it can happen. Just flip your phone to another side while you create it (meaning your entire interface will get recreated for your landscape view), and you can get this crash:
Leaving the first part of this horrendous call stack here for your viewing pleasure. There would’ve been two more pages of it, but suffice to say the private SwiftUI code eventually enqueued the drag operation that asked for the async callback.
Because I got scolded multiple times by this, I got two tricks under my sleeve. I made sure I could change all my DispatchQueue.main.async
to an asyncAfter
for a two seconds delay
This is how I reliably got that crash – and if you say I cheated, it means you know nothing about multithreaded programming: what can happen will happen, even for longer delays. I actually had that type of crash multiple times before, and they are horrible to track and horrible to fix.
Why does this happen? It happens because I got a view that’s now defunct, and my asynchronous call doesn’t know that. So I now need some other way to do this.
@State var initialFrame = CGRect()
@State var work: DispatchWorkItem? = nil
var body: some View {
Spacer().overlay(GeometryReader { geometry in
Color.clear.onAppear {
self.work = DispatchWorkItem {
self.initialFrame = geometry.frame(in: .global)
}
DispatchQueue.main.async(execute: self.work!)
}
.onDisappear {
if let work = self.work {
work.cancel()
self.work = nil
}
}
})
}
Code language: Swift (swift)
But oh, bollocks, you remember the reason we added the async? Yeah. Because you cannot set state in onAppear
in this instance somehow. So … let’s try to keep the variable in our function …
@State var initialFrame = CGRect()
var body: some View {
Spacer().overlay(GeometryReader { geometry in
let work = DispatchWorkItem {
self.initialFrame = geometry.frame(in: .global)
}
return Color.clear.onAppear {
DispatchQueue.main.async(execute: self.work!)
}
.onDisappear {
if let work = self.work {
work.cancel()
self.work = nil
}
}
})
}
Code language: Swift (swift)
Which results in a
… So I could return AnyView
of the thing, which is against my principles, or I could put that code outside in a function.
@State var initialFrame = CGRect()
func setInitialFrame(_ geometry: GeometryProxy) -> some View {
let work = DispatchWorkItem {
self.initialFrame = geometry.frame(in: .global)
}
return Color.clear.onAppear {
DispatchQueue.main.async(execute: work)
}.onDisappear {
work.cancel()
}
}
var body: some View {
return ZStack {
Spacer().overlay(GeometryReader { geometry in
self.setInitialFrame(geometry)
})
}
Code language: Swift (swift)
And now, finally, it works! Yay! It reliably works. But it wouldn’t be over the top if it would work like that all the time, isn’t it? So what if that piece of code actually happens at app start or the view gets coerced in being shown when the window is in the background? (Don’t laugh, I had this happening quite reliably in some cases)
This means I must first verify whether I am active or not, then I can do things, with this piece of code
guard UIApplication.shared.applicationState == .active else {
return
}
Code language: Swift (swift)
Can I do this? Well, yes I can. So I must actually call the piece of code in onAppear
, then call the work item yet again if I am not active on the next frame, while allowing cancellation. I leave that one to your imagination. And because the work
variable is actually inside a lambda from a struct that might get destroyed and recreated, it means I can still get that crash. So I ended up creating a manager to keep up my asyncs through AnyCancellables
objects, all of that merely to bypass something that should’ve taken me two minutes.
See, the problem I have with these issues is not that it’s impossible. It’s that for a very simple concept, I have to use very convoluted pieces of code through a trial of fire, or face sporadic runtime failures, which is something the Swift language worked really hard to make nearly impossible to happen, by design and default. And SwiftUI made this appear back in my face all over again.
A silverlining: Use Anchors
And … people who read this (you are a trooper, really!) who went through these steps have probably chanted the same line since the beginning of that part: use Anchors! Because the way you actually get your initial position is to have a second clear object in the background, define it as your anchor point, and then compute the difference position between your anchor and your new object. This will reliably work. No, really, this time, this is the actual way!
How can you know Anchor
even exists? How can you tell how to use it? Are there examples? The answer, in case of SwiftUI, is always a resounding no! Because the documentation, as currently provided, only tells the evidence, but will not give examples or how to use something. Since the beginning of SwiftUI, you are expected to forget how you would code something, and learn how the SwiftUI designers would do something. But they provided very little clues, a few talks and a few basic tutorials on their page, and nothing in the documentation. This is a screen capture of what they provide to help us:
And this is good for everything in the documentation: it will tell what it is, but not how it does its magic. You actually have to look at StackOverflow as well as a few dozen awesome coding web sites who are trialing things by fire in order to figure out how to do very basic things.
It’s even worse when it animates
Because life is unfair, even if you do everything by the book, know how things should be done, and use it correctly where it doesn’t ever crash because of your code, you will still get instabilities. For example, if you start abusing the system, if you start having a list with animated object in them, and you start putting your phone in background mode while they happen, you will still get crashes. Now I know these crashes are because I am doing something incorrect… But what?
This is my error message:
2020-02-23 12:45:32.413862-0500 CreaPhoto[2662:674126] precondition failure: attribute failed to set an initial value: 407
And this is my call stack alongside the breakpoint:
These are the kind of things that really annoys me to no end, because SwiftUI framework is actually private, I get an internal crash about some kind of instability, and I have no clue where that instability might be, or even where I can start to look for it. Obviously, if my software crashes, Apple will rightfully decline my app from the App Store, as my software is instable. And that, even if I am using their own framework, and it’s their framework that ultimately crashes without any means for me to rectify that issue.
Because of that, I actually decided to forego animations entirely, unless I have no choice in putting them in. And I will hope for the best that end users will not get such crashes. So far, I was frugal enough in animations I have zero crash logs about these. But let me tell you I worked really hard in keeping only what is necessary. And for many of these, when I switch to background, I actually have an if-else on my entire view to an EmptyView()
, just because it was actually impossible to keep it. It’s ugly, but at least, it doesn’t crash!
In other words… TL;DR SwiftUI
- removes all the stability points of the Swift language;
- is patently unstable;
- has no real official documentation;
- has weird unexplained contraptions;
- needs you to figure out how they would do it otherwise you are in a world of hurt;
- will crash internally without ways for you to figure out what you do wrong;
- is missing a lot of basic Mac, iPad, iPhone components.
Should I use SwiftUI? Yes, learn it, it’s really the future! But you might have issues, so you are better waiting for the last major version to create something meaningful.
My next article will be about my macOS / macCatalyst port. And you will see it’s not better!
So why wait for v2.0? Why not just dropping it all? Because now that we have an actual version that works, I am sure the next major version of SwiftUI will actually fix most of these things. And seriously, the developers work really hard in actually making something next gen and awesome.
This article is bound to be defunct soon, to merely be an artifact of the past. One day, I wish SwiftUI will have enough sugar to help develop efficiently in a way I feel comfortable, in my own style. Not the one the lead SwiftUI designer decided. In 1.0, it works, and that’s already huge! I have full confidence in them it will get better.