I’ve just released Acacia, my first SwiftUI app. It’s a practice tracker aimed at musicians, for iOS, iPadOS and macOS. More info on it here and you can grab it on the App Store now. This post covers some of my ups and downs while learning SwiftUI.
TL;DR: SwiftUI is an intoxicatingly-pleasing and fast way to create apps. Polishing these apps is hard, and can quickly become energy-sapping when trying to finish projects. But, after pushing through a few failed attempts, it’s completely won me over and I can’t see myself returning to UIKit.
It should go without saying that everything below reflects my opinion and personal taste. I think any tools that empower you to create what you want to create are valid, and nobody should feel bad about their choices (or at least no individuals, corporations could probably try harder). This blog is written in PHP, which I learned in 2018 and love using. If your choice is between creating something using the most-derided tools, and not creating something at all, choose create.
I started learning to program in 2016, which I’ve written about before. The short version, though, is that I cut my teeth writing iOS apps using Swift and UIKit. UIKit really clicked for me, though I never enjoyed using Interface Builder and for the first year or two I did any complex layouts programmatically. Eventually I relented and started using IB a bit more. At first primarily for scaffolding, but eventually used it more or less everywhere I could (practically) do so. In spite of the time saved on laying things out for multiple screens and the obvious wins it brought, it was always slow, frustrating and I felt like I wasn't so much using it as wrestling with it.
When it comes to the Mac, I’ve only dabbled briefly in AppKit in order to make utility apps for myself. As much as I love the general idiom of macOS, I find AppKit an old-fashioned and pretty unpleasant framework to use. No surprise, when I was already used to its newer, shinier sibling. I never really considered Catalyst an option for developing Mac apps; it seems to require as much or more finagling as SwiftUI to leap the (albeit increasingly narrow) idiomatic canyon between macOS and iOS. But more to the point, in the main, I don’t think Catalyst apps are very good (at least not without a tonne of extra work), and I'd rather spend that energy on something more exciting.
I was really excited to see the SwiftUI announcement in 2019. It felt like an entirely different, yet familiar, approach to UI, and I couldn’t wait to try it. At the time my day-to-day work involved maintaining apps that were written using UIKit and IB, and given my preference for goal-oriented learning, I ended up not finding a good opportunity to learn SwiftUI that first year. Arguably that’s probably a good thing; Swift itself was already a couple of years old by the time I started learning it, and even then it was frequently changing in ways that made starting out as a programmer more cumbersome than it might otherwise have been.
Eventually, around July 2020, I had an idea for an app that felt like a good fit for learning SwiftUI. There were now the shiny new improvements from WWDC 2020 that made me even more exited to use it. By that time I was also maintaining web projects for the most part, so wouldn’t have to switch back and forth between SwiftUI and UIKit/IB. Spoiler alert, this app was not Acacia, rather one I working-titled Headway. It was a tangentially similar idea that had that (im)perfect combination of a hazy idea of its target user, and an infinite, unordered list of features it would need to appeal to whoever that potential user might end up being.
Using Apple’s learning materials to get started, creating UI was just as fun and intuitive as I’d hoped it would be. It didn’t take long for me to have all the ‘screens’ for the app prototyped. While the ‘reactive’ approach makes a lot of sense, it was a major shift in mindset from what I’d done before. The advantage was, though, that the data model was more or less figured out in tandem with the UI.
The problem that happened at this point, in addition to the lack of direction within the app itself, was that I’d built a pretty elaborate UI that only mostly worked. Thus began the Sisyphean task of fixing all the jank. At every turn, new and unexpected UI bugs would pop up; navigation hierarchies would break, sheets and alerts would randomly trigger or dismiss, the UI wouldn’t update to match the data, and other times it would. I was never quite sure whether I was doing something wrong, or whether it was some SwiftUI bug (and there are plenty of those). In the end I lost momentum, set aside and eventually abandoned the app.
At a surface level, SwiftUI provides a very compelling illusion of being easy, but it’s not. The degree of conceptual simplicity is a huge draw, but despite having a pretty strong grasp of Swift there’s just enough syntactic newness—dollar signs, @Thingies and underscores—that I ended up relying on rote structures I didn’t really understand. So when things got beyond a certain level of complexity, and broke, I wasn’t able to fix things. Or at least I wasn’t able to fix them in a repeatable, useful way; I could change things until they worked but I’d not really have learned anything by doing so.
In the final week of February 2021 I had a new idea (this one would turn out to be Acacia). It was much simpler, and I had a good sense of the scope of features v1 would need. So I got working, this time proceeding much more carefully. I’d learned a lot of SwiftUI from my previous excursion, and was starting with a much more logical idea of how the code should be structured. I wanted to use SwiftUI exclusively, and if possible to avoid the rabbit hole of bridging in tonnes of UIKit/AppKit views.
Within a week or so I had the app more or less working—animations, iCloud sync, the works—but there were still lots of rough edges. For one, the macOS version was a mess. I had as a general rule used only SwiftUI code that worked on macOS and iOS, but there were still lots of things that simply didn’t work or were chaotically laid out. Far from a magical way to get a macOS app ‘for free’. On top of that, there were lots of glitches on the iOS app; navigation views would randomly break, toolbar buttons disappeared, there was no way to dismiss the on-screen keyboard, and a lot of these things seemed to be outright impossible to sort without resorting to UIKit/AppKit.
Acacia is about 3,200 lines of code according to
cloc, and within that I have 49 blocks of code that are behind the
#if os(macOS) compiler directive, and 55 that are behind
#if os(iOS). At first I found this really clunky, because I didn’t have a good sense of where these fit; it took me a while to grok whether they could go inside parentheses, or closures, or even to figure out the cryptic error messages I’d get when I placed one wrongly. But after some persistence, it began to feel quite natural. Instead of trying to shoehorn as much of the iOS layouts into the macOS app as I could, I started to modularise Views into the smallest units that would work on both platforms. Then I composed parent Views that used these directives to display the right layout on the right platform.
The above is probably pretty obvious, and Apple’s own tutorials advise that breaking code into subviews is a very cheap and effective way to write clear SwiftUI code. But with Headway I had built so much of it by rapidly learning, iterating, and tweaking things till they looked right, that I didn’t even know where to begin when trying to decompose Views into sensible chunks. Switching the build target to macOS after the initial iOS structure was laid out was actually a big help here. Settings aside the dubious merits (both economic and functional) of actually having a Mac app, the places where I had to swap out code for each platform were a really good guide at achieving a happy medium between huge, monolithic Views, and unnecessarily trivial ones.
I mentioned that I wanted to keep Acacia as close to ‘pure’ SwiftUI as I could. The short version is that this, unless I’ve missed something, isn’t feasible yet. The lack of some things, like equivalents to
registerForRemoteNotifications() meant I had to call out to the underlying NSApplication/UIApplication to do a few things. In the end, though, the only times I had to use
UIViewControllerRepresentable were to use
SFSafariViewController, and to (bafflingly) have text inputs that allowed for a ‘Done’ button so they could dismiss the keyboard. I’ll be shocked if that latter requirement survives past WWDC 2021.
I described SwiftUI as intoxicating at the beginning of this, and I think that’s the key theme I’ve felt when using it. That intoxication has its counterpart in the wearisome nature of pinning down and removing quirks, but on the whole this abated quickly as I got more proficient. My key takeaways for anyone wanting to try it out are:
I could've built Acacia using UIKit, but this was more fun and I learned lots. Likewise I could have built it for macOS via Catalyst, but I almost definitely wouldn’t have. While I feel like I've only just got the hang of the basics of SwiftUI, now that I’ve broken the back of it, it feels great to be equipped for what’s next.
Some music I listened to while writing this: