Swift’s new concurrency support comes with two promises: making it easier to write asynchronous and parallel code with new syntax, and making it safer to do so with new static checks. The new checking is still a work in progress, partly because of the massive impact it will have on Swift code, but worth checking out if you’re working with async/await or actors.
In my previous post, I shared a tiny toy HomeKit app which used a bit of Swift Concurrency. Although the app logic itself is single-threaded, it uses async calls and task groups to kick off HomeKit calls and collect their results. I tried it out with Xcode 13.3 beta 1, and fixed all the new warnings it gave me (diff). I thought it was worth writing about; the code is better, and easier to parallelize if I want, in exchange for some busywork making things threadsafe which don’t actually cross threads.
Sendable checking
In Swift, something which is sendable may be safely sent from one concurrent context to another (passed into a task, or passed between actors, such that it might switch threads or be run in parallel with the sender). A big part of the new checking is verifying the sendability of things when required.
A data type is marked as being sendable by adopting the Sendable
protocol;
this requires that it not hold a reference to any non-synchronized mutable
state. A function type is marked as being sendable with the @Sendable
attribute; this requires that it not capture any non-sendable data. The
compiler can also infer sendability in many cases, so you don’t always have to
mark it.
My task bodies were, indeed, capturing non-sendable data, including my own Bulb type and all of HomeKit’s types. (Neither HomeKit nor Foundation nor any other Apple API is, at this time, marked up for sendability.) This had been generally harmless as written, because the data was being sent from the main thread to itself. Nonetheless, I fixed it by encapsulating each lightbulb’s HomeKit operations within the Bulb type, and isolating that type to the main actor. (Actor-isolated types are implicitly Sendable, since any mutable state is protected by that actor.) The result of this refactoring, unsurprisingly, was a cleaner model anyway.
Protocol implementation isolation checking
Protocols can also be marked as actor-isolated or not, and a non-isolated protocol requirement can’t be given an actor-isolated implementation, for very good reasons.
Identifiable
, for example, makes no statement about concurrency, so its id
property may be referenced from anywhere. Once Bulb
was marked @MainActor
,
its var id
became isolated to the main actor. Because it was mutable state,
other actors or non-actor contexts would be unable to safely access it. Making
it let
solved the problem.
I also implemented two methods from HMHomeManagerDelegate
on my
main-actor-isolated model class. This warning isn’t just a technicality;
neither the Swift compiler nor I have any idea what thread HomeKit will call
these methods from. So I marked them as nonisolated
, escaping the main-actor
isolation of the class, and, internally, used Task { await reload() }
to
asynchronously call the isolated reload()
method; that method will hop back
on the main queue if needed.
From PartyLights to the real world
It was nice to have a toy app to try these things out on. I next tried the beta with 3MC, which needs a bit more work, and which emitted several warnings which I don’t think I can resolve until Apple updates more of its frameworks. I next tried it on TimeStory, which doesn’t use any async/await yet, but the compiler just crashed. So it goes, with betas.