Using Swift's new async/await when invoking Kotlin Multiplatform code12 Jun 2021 Share on:
One of the biggest, and probably most anticipated, announcement at WWDC last week was regarding Swift 5.5’s new concurrency features and, in particular, its new async/await mechanism. This would appear to work in a way that’s very similar to Kotlin Coroutines which should I believe make Swift based async development significantly more approachable for Kotlin developers but also might provide option to allow a more seamless integration between Swift code and Kotlin suspend functions exposed from KMM shared code. This post is primarily aimed right now at capturing notes around exploration of using async/await mechanism to wrap/invoke calls to shared Kotlin code and also to look at what would be involved in potentially propagating cancellations in Swift async code to underlying Kotlin Coroutines. Have more questions than answers right now but hopefully that will change and will update this post if/when it does. (Update: see note at end of post).
What's new in Swift 5.5? It's easier to ask what *isn't* new in Swift 5.5, because so much is changing – async/await, concurrency, actors, throwing properties, CGFloat/Double bridging, local lazy, property wrappers for function parameters, and more 🤯 https://t.co/nYbjOYKWKu— Paul Hudson (@twostraws) May 28, 2021
Current mechanism to invoke Kotlin suspend functions from Swift
Kotlin 1.4 introduced the ability to effectively call Kotlin suspend functions directly from Swift. Swift of course knows nothing about Kotlin Coroutines or suspend functions so this is done by generaring code such that the method appears to the Swift code as one that’s passed a completion handler that’s invoked with success/failure info (with Kotlin/Native providing the associated mapping under the hood). For example, if you had say following function in shared Kotlin repository class (based on PeopleInSpace project)
Then this would be invoked from Swift using something like:
This is definitely a useful capability to have but, as outlined in this article, has a number of limitations including for example fact that we have no control over the coroutine scope/dispatcher used and also no way to cancel the coroutine (more on that later!).
So, with this in place, an initial step might be to at least wrap this invocation in such a way that it at least appears to other Swift code
async function. A Swift async function btw is described as follows (sound familiar? :) ):
An asynchronous function or asynchronous method is a special kind of function or method that can be suspended while it’s partway through execution.
The way we can achieve this mapping is by using
withCheckedContinuation as shown below.
Which can then be invoked for example like this:
Also announced at WWDC were some async related updates to SwiftUI. These include for example new
task closure that allows invoking
async functions directly and that is fired when a view appears and automatically cancelled when the view disappears (somewhat analogous to the
behaviour of lifecycle aware coroutine scopes that are part of Android Fragments/ViewModels etc). Also added was automatic “pull to refresh”
capability that can be added by adding
refreshable which again also allows invoking async functions directly long with associated cancellation
What we’d ideally like to do is get notified of cancellation and propagate that cancellation to underlying Kotlin Coroutines. Firstly, another definition:
A task is a unit of work that can be run asynchronously as part of your program. All asynchronous code runs as part of some task.
It’s at the task level that cancellation occurs and to get notified when one is cancelled we can use
withTaskCancellationHandler. But this
is where the story ends for now. Given the current mechanism used to call Kotlin suspend functions from Swift we don’t have access for
example to the associated job used (so we could cancel it as shown for example in following). Another possibility here is to
not expose suspend functions from Kotlin shared code but to launch them separately and then cache/expose associated job (as described for example
in another article here where we map a Kotlin Flow to a Combine Publisher)….seeing some issues here however from initial exploration of
doing this (related to synchronisation with associated Swift async function). Will provide updates here as things develop!
One other somewhat random observation was that if you mix approaches and pass say a success handler to your suspend function like following
Then XCode offers a “consider using asynchronous alternative function” prompt and you can then invoke that method using something like:
This isn’t likely a common or useful approach but interesting to see nonetheless…I’m assuming this is
based somehow on functionality that Swift is providing to allow use of
await with “legacy” Objective-C etc functions that use completion handler.
Using Rick’s approach I added following to class in
And then updated
getPeopleAsync as follows. Now if we say navigate to different view while this is in progress (assuming we’re calling
task as above) then cancellation is propagated to shared Kotlin code and we see cancellation exception error printed out in the catch shown.
Featured in Kotlin Weekly Issue #256
Wrote a short article about this exploration of mapping Swift's new async/await capability to #KMM shared code....more like development notes right now with more questions than answers but will update as things develop!https://t.co/GjLDkaKnL7 https://t.co/M6inJPxdiJ pic.twitter.com/gy8NtelUaJ— John O'Reilly #BlackLivesMatter (@joreilly) June 13, 2021