Over the past several days, I built a complete, functioning app in SwiftUI, and, well, I have some thoughts.
The app I built is a little TOTP app that works exactly the way I want, because Authy is bad. And it plays to SwiftUI’s strengths pretty well. It’s got a straightforward UI, with very few custom interface elements. The data model is simple and how it interacts with the interface is clearly delineated.
For the most part, writing the app has been a good experience. The vast majority of it I wrote in about 20 hours worth of work spread over four days. As of writing this, it’s about 1700 lines of Swift. The app itself is open source here, so if you want to see the codebase that I’m rambling on about, you can take a look.
The process was generally enjoyable. It’s what everyone says about SwiftUI: you can iterate incredibly quickly, there’s vastly less boilerplate than UIKit, and it’s much easier to build simple, reusable views.
This was also the first project where I actually managed to use the Xcode Previews feature. Although it always seems slick in WWDC demos, I was never able to get it to work reliably. But this time was different. The fact that Xcode pauses previews whenever something outside of the view body changes remains annoying, but at least while I was only modifying the body it did update reasonably quickly. I guess it really just works best for smaller projects.
Building almost all of the UI was easy. The design is simple enough that it doesn’t have to use any fancy tricks or workarounds—not a
GeometryReader in sight. Plumbing up the model was similarly painless, both for editing and displaying data (aside from some oddities involving timing and refreshing codes).
But, here’s where the complaints start. Although the layout and design were uncomplicated, building some of the interactions were not. While working on it, SwiftUI felt incredibly powerful and uncomfortably restrictive simultaneously. For example:
Want to make give a context menu action the destructive style? Ok. Want to make a nested menu destructive? Nope.
Want to drag and drop one view onto another view? Go ahead. How about dropping that view onto one that’s inside a
List? Woah there.
Making a text field focused programatically? Sure. And what about making it focused immediately on appearance? Not so fast.
To be clear, many of the issues I’ve encountered with SwiftUI are almost certainly bugs that will (hopefully) be fixed eventually. But where this differs from UIKit is that the architecture of UIKit lets you fix and workaround things yourself, whereas SwiftUI operates as a black box.
To use the same examples, but in UIKit: to make a context menu element destructive or not is exactly the same regardless of whether it has children, adding a drop interaction to a UIView is the same inside a table view as it is outside, and focusing a text field uses the same method no matter when in the lifecycle it’s called.
I don’t think this is just because I have more experience with UIKit. It’s not that there are lots of UIKit tricks I know to make this work, in constrast with SwiftUI. There is a fundamental difference between the two frameworks. The delta between doing any of the things I mentioned in one context versus in the other is zero. Because of the fundamental design of UIKit, all of these things basically just work. Yes, implementation wise, the framework engineers may still need to take special care to ensure that they work. But, from an API design point of view, nothing different is required.
The impression I get from Apple is that SwiftUI wants to make hard things easy. That’s a great goal, but currently it comes at the expense of making easy things complicated. And every time I do anything substantial with SwiftUI, I constantly feel this tension. SwiftUI both empowers me and hinders me, every step of the way.
I don’t know what the solution is, if there is one (part of me thinks these sorts of issues are intrinsic to declarative tools), but I really hope there is one. When it works, I really enjoy SwiftUI and I want to be able to enjoy it more without running into unresolvable framework issues quite so frequently.
It only shows one code at a time and requires too many taps to switch. It also has no way of exporting or backing up the TOTP secrets. ↩