Learning SwiftUI

img

My new SwiftUI app, Acacia

Some context

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.

Finally, I’m not going to be covering any specific code examples in this, so it's not going to be much use for fixing bugs. There are far better sources for that information than here.

Where I’m coming from

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.

img

The storyboards for a past app, don't they just scream fun?

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.

img

Extempo, an AppKit app I made years ago during my PhD, for doing computer-assisted improvisation.

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.

Starting out with, and giving up on, SwiftUI

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.

Trying again

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.

Embracing #if

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.

Respecting one’s elders

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 resignFirstResponder() and 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 UIViewRepresentable or 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.

Closing thoughts

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:

  1. Proceed with a goal. Without one it's very hard to get through the slump in motivation that follows the initial development phase.
  2. Be prepared to start over. Probably more than once.
  3. Make it a Mac app, too.

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.

In the meantime, check out Acacia on the App Store, and read more about it here.


Some music I listened to while writing this:

SwiftUI Core Data bug

I've been building an app using both SwiftUI and Core Data for the first time. It's been a journey. Among a lot of weird gotchas, bugs, and new paradigms, I've just pinned down a bug that's been causing me a tonne of grief.

I'll try and explain the setup as concisely as possible:

  1. A sheet is presented which takes some user input, and which has a Core Data object (let's call it ParentObject) passed to it from the parent view.
  2. The user taps save, and a new Core Data object (let's call this ChildObject) is instantiated. The values they've input are assigned to that object, and its relationship as a child of ParentObject is set.
  3. With seemingly no pattern, this save operation fails and crashes the app with the error "Illegal attempt to establish a relationship 'Parent' between objects in different contexts."

I went through everything I could find or think of to pin down the issue. I found plenty of red herrings—certain inputs or sequences of deleting and re-adding objects in quick succession—but none of these held up to repeated testing. Then I noticed one pattern, which persisted with any testing I did.

The crash happens any time the 'slide-to-dismiss' gesture was initiated and cancelled. It didn't matter if it was only by a few pixels, if the sheet experienced any downward movement at all, the Core Data context passed down through the @Environment wrapper was lost/changed and any subsequent save operation failed. Because the sheet in question contains a ScrollView I'd accidentally triggered the swipe-to-dismiss gesture countless times, and it was basically a flip of the coin whether I'd have done it each time I was running through the steps to replicate the bug, I've send a Feedback (FB9048688) so hopefully it gets sorted soon.

Update: I did figure out a workaround to this just now. Getting the parent object's managedObjectContext, rather than using the @Environment variable, and using that to create the child object seems to work fine.

One Shoesworth of Running

When starting out with a new hobby, I like coming to milestones and looking back on my progress. Often these are just arbitrary dates, like one year in, but in this case a worn-out pair of runners seems like as good a time as any. I went for my first run on 27 April 2020. It was near the start (or as I had thought at the time, the end) of Lockdown, and came at a point where I was probably the least fit I'd ever been in my life.

324 days later I've run a total of about 750km. I started off with two- or three-kilometre runs a couple of times a week, at first very early in the morning around a 200-metre loop of footpath near my house. I worked my way up to five-kilometre runs over the first few weeks, then felt confident enough to run a 5k route through town by the middle of the summer. As I find with a lot of new hobbies, after the initial feeling of success I got a bit less diligent; I was managing one or two runs a week by the end of the July, and by September I was letting weeks go by without running at all. On 27 September, conveniently yet coincidentally 100 days before 1 January, I went for a run in the evening. Then did the same thing on the 28th. And when I realised keeping that up would get me to a total of 500km by the new year, I decided I wanted to do just that.

The sense of progression I felt during this period was great. My 5k record had been around 32 minutes at the beginning, and was about 22 minutes by the end. I missed my first day during October, and decided to try running a 10k the next day to maintain my average. By November I'd run four 10ks in a single week, and over the remainder of the time my best time fell from 56 minutes to 48. In December I managed a new distance record, stopping at just over 16km or 10 miles.

I reduced my intensity a little in January—running in icy wind had lost its charm around the 501st kilometre—then took a two week break in February to recover from some leg pain I didn't want to exacerbate. In March I returned to my aim of 5k per day and have kept going since.

DMDB

As per its introductory paragraph, I often find myself wondering when watching movies, particularly old ones, how many of these people are still alive? So of course, a bit of code and some profuse use of The Movie Database's API later, DMDB was born.

It pretty much does what it says on the tin: you can search for movies and it'll tell you what percentage of their cast is alive or dead. I've also compiled a list of some interesting outliers and coincidences (Se7en is my favourite one so far).

Check it out here.

Vaccine Progress Tracker

After seeing the UK Vaccine Progress Tracker Twitter account yesterday, and already being a fan of the Year Progress one, I had to make an equivalent for Ireland.

Much like its UK counterpart, it's written in Python using the Tweepy API. Mine scrapes data from Ireland's COVID-19 Data Hub and will tweet only when new data has been added. Annoyingly, this source data seems to lag behind the "headline" data that gets reported in the news, but I haven't found any better official source.

Gauging Population Over 18

Like the UK bot, I'm counting percentages of adult (18+) population, those being what the government roadmap focuses on. This was an interesting challenge because there isn't really a solid source for the number of people older than 18 living in Ireland. The most recent census was 2016, so its data is about as out of date as it's going to get. It states the population as 4,761,865 but uses a fairly inconvenient set of age groups—13-18 and 19-24—which creates a bit of grey area.

In the end I estimated that about 1/6 of the 13-18 age group would be 18, giving an approximate over-18 total of 3,572,000 or 75% of the total population. The most recent official estimate of total population comes from the Central Statistics Office and places the population at 4,977,400 as of April 2020. Wanting to incorporate this growth, I used the 75% proportion from the 2016 data to land on 3,733,679 as the number that I base the 'progress' percentage off. If anyone knows of a better estimate, let me know!

Update: Switching to Over 16s

To reflect some changes to the vaccine rollout in Ireland, I've now slightly modified the tracker to measure against the population aged 16 and over. Using the same method as above—just taking 1/2 of the 13-18 age ground instead of 1/6—I arrived at a new figure of 3,863,147 to base the percentage on.

Update 2: Revised Over 16s Figure

Using this tweet I've derived an estimate of 3,909,809 for the total over-16 population, so have updated the bot to use this going forward. It's nice to have something closer to an official figure now, and it's a relatively small change from what I'd been using before, so won't make a huge difference to the percentage so far.

Typos

Time wasted today because of typing request.onReadyStateChange instead of request.onreadystatechange: a few hours more than I'd like to admit.

Incompletionist

I don't tweet often (I think about 5,000 times in the past 12 years), but I was very much a Twitter completionist. I curate the list of people I follow fairly tightly, and barring a few muted people, read every single tweet. Tweetbot was really handy for this, for keeping my position in the timeline and syncing it across my devices. After about a decade of this, I found myself checking Twitter reflexively: waiting for a file to download? Check Twitter; waiting for an app to compile? Check Twitter; waiting for someone in a coffee shop? Check Twitter.

Realising this was doing me no good at all I quit cold turkey on Sunday. I deleted Tweetbot from my devices, and aside from going on the website a few times to check something specific (like the delightful absence of a certain 'real' guy's account), I've not been on it since. While I like and am interested in most of what the people I follow have to say, for the most part I also don't really care about it, and was giving it undue presence in my mind.

I've replaced the habit with a combination of reading on the Kindle app, reading in NetNewsWire, or just being alone with my thoughts. I'm genuinely surprised how little I miss it, and whether I go back or not, the completionist days are over.

Running

I started running in April, during (the first) lockdown. I used to love going for long walks, but wanted to up my fitness and running seemed like a good option. One thing that bothered me about walking is that I'd often end up checking my phone, replying to messages, or even idly checking Twitter while I walked; walking in itself didn't necessarily require all of my attention. After a few months of running a couple of times a week, in the last week of September I decided to try running 5km every day for as long as I could.

I've been averaging pretty much exactly 5k per day, though with a few days off and a few 10ks to compensate. To my surprise, I actually love doing it. One big reason is that I have to do run and only run; there's no phone-checking, nor even the temptation to do that. It's just me, some music or a podcast, and the constant task of brushing my sweaty hair out of my eyes.

Sext

The fifth service of the Divine Office, usually performed at noon. The service consists of several responsories and psalms which are sung.

According to OnMusic Dictionary. Why, what did you think it meant?

Civilizations

I've gotten hooked on a fantastic YouTube channel and podcast recently, called Fall of Civilizations. It produces multi-hour-long documentaries about great ancient cities and cultures, and their decline. The one on The Sumerians is a great starting point.

The rise and fall of civilizations is such a fascinating topic, and taps into what feels like such a root theme for humanity. Everything we make has its rise, glory days, decline and collapse; this applies equally to TV shows, political systems, companies or indeed entire civilizations. Unlike most animals, we architect our own rises and falls, and constantly push and pull at equilibrium rather than drifting along with it.

It seems like the speed at which this happens to a civilization correlates with the rate of technological advancement within it. It's mind-bending to me that the Sumerians fell victim to an ecological disaster that unfolded as a result of millenia of broadly similar farming and irrigation practices. It's incredible that their habits lasted through enough generations to cause problems that only emerge after double-digit centuries. Modern-day farming has changed drastically in just the past 50 years.

What's interesting, though, is that humanity on the whole forms a much more resilient metastructure. Our transient creations emerge from the interaction between thousands, millions or billions of free agents, and seem inevitably to fall victim to what I describe above. But humanity itself is far more resilient; the comprising civilizations, the ones that weather their falls, are there to start afresh each time.

Folklore

Taylor Swift's new album is excellent. I've listened through quite a few times at this point, and it's good from front to back. The last track, Hoax, is my favourite I think.

Tabs

The entire tab experience of Xcode 12 is perhaps the most infuriatingly unintuitive software experience I've ever had.

all posts
©2021 Stephen Coyle