Swift deinit with Strict Concurrency
Deinit in strict Swift concurrency behaves differently from prior Swift versions and might cause unexpected compile or runtime issues. I’m getting annoyed with it :)
As I continue my research and work on modernizing the RIBs framework and adding strict Swift Concurrency support to it, I stumble upon things I would’ve never otherwise.
One of them is how deinit behaves with the new strict Swift Concurrency.
Apparently, if you have a @MainActor annotated type, its deinit will be assumed to execute in a nonisolated context even though the type is @MainActor. Supposedly, this is because the object can get deallocated (its last retain count reference released) on any thread, not only the one it’s isolated for (the main in this case).
This becomes an issue if you have any type of cleanup logic in your deinit where you need to free up the resources your object is holding on to or to call any cleanup logic such as closing streams, open connections, subscriptions, etc. The compiler now will yell at you and tell you that you can’t be calling methods on self in deinit. Even if you wrap self in a weak self and run the methods via a Task { @MainActor in } wrapper, it still would not work because by the time it hops to the threads main thread, your weak self reference is going to be nil.
Now, to me this was a huge surprise and a big pain for two reasons:
In my application codebases, I never ever ever used deinit as a logical cleanup. It just doesn’t make sense and is very error-prone to let the Swift language/framework control the flow of application logic. An explicit cleanup at the appropriate time/lifecycle is always much better.
Now I am forced to deal with this issue because I need to modernize the RIBs codebase and make it compatible with the new strict Swift concurrency.
The way deinits are used under the hood of RIBs is for data cleanup and lifecycle propagation (such as letting all the children RIBs know that the parent is detached/deallocated so that they do their own cleanup) and for internal RIBs reference counting to keep track of memory leaks.
I’m exploring different solutions. I can’t use isolated deinit because I need to support older iOS/Swift versions, but most likely I’ll end up refactoring away from deinit entirely and have explicit cleanup methods when RIBs are detached. That way I can keep the same isolation context and have no restrictions on method access and be guaranteed that my object is still in memory.
Conclusion
The more I dig into Swift Concurrency and the new strict rules, the more I get annoyed with it. I understand the desire and the good intentions behind it—let’s help developers with concurrency thread hopping so that they don’t call things on the wrong thread (such as UI updates on a non-main thread) and don’t have as many race conditions and other subtle concurrency problems.
But the approach Apple took with Swift seems to me too rigid and prescriptive. Better education of the Swift community on better design patterns and architectures that avoid these threading issues would’ve sufficed. Now, instead, we all are struggling with compiler issues caused by strict concurrency rules because we want to help a few devs to write better code.


Possibly the hardest issue I've ever debugged was a problem where I kicked off a task in a deinit.