Kotlin Multiplatform allows us to share code between different platforms, including iOS where Swift is the primary language. Both Kotlin and Swift have evolved to support modern asynchronous programming patterns - Kotlin with Coroutines and Swift with async/await and structured concurrency. However, their approaches and APIs differ significantly. In this article, we will explore how to make Kotlin Coroutines work seamlessly with Swift's async/await, and how to bridge between these two concurrency models.
Understanding Swift's Async/Await and Structured Concurrency
Swift introduced async/await in Swift 5.5, along with structured concurrency features like Tasks, TaskGroups, and actors. This modern concurrency model shares many similarities with Kotlin Coroutines, but there are important differences:
Similarities:
Both use cooperative concurrency instead of preemptive threading
Both support cancellation and structured concurrency
Both provide async/await syntax for sequential asynchronous code
Both support concurrent execution of multiple operations
Key Differences:
Swift uses Task for unstructured concurrency, while Kotlin uses launch and async
Swift has built-in actor isolation, while Kotlin relies on thread-safe data structures (it also supports actors, but this support is limited, rarely used and marked as obsolete)
Swift's async functions are marked with async, Kotlin uses suspend modifier to mark functions that can suspend the coroutine and launch and async to start coroutines
// Swift async functionfuncfetchUserData(userId:String)asyncthrows->User{let response =tryawait networkService.get("/users/\(userId)")returntryJSONDecoder().decode(User.self, from: response)}// Swift Task for unstructured concurrencyTask{do{let user =tryawaitfetchUserData(userId:"123")print("User: \(user.name)")}catch{print("Error: \(error)")}}
// Kotlin suspending function
suspend fun fetchUserData(userId: String): User {
val response = networkService.get("/users/$userId")
return Json.decodeFromString<User>(response)
}
// Kotlin coroutine for unstructured concurrency
scope.launch {
try {
val user = fetchUserData("123")
println("User: ${user.name}")
} catch (e: Exception) {
println("Error: $e")
}
}
Converting Kotlin Suspending Functions to Swift Async Functions
Kotlin/Native automatically converts suspending functions to Swift async functions when they are exposed to the iOS target:
// Shared Kotlin code
class UserRepository {
suspend fun getUser(id: String): User {
delay(1000) // Simulate network call
return User(id, "John Doe")
}
suspend fun saveUser(user: User): Boolean {
delay(500) // Simulate save operation
return true
}
}
This becomes available in Swift as:
// Generated Swift interfaceclassUserRepository:KotlinBase{funcgetUser(id:String)asyncthrows->UserfuncsaveUser(user:User)asyncthrows->KotlinBoolean}// Usage in Swiftlet repository =UserRepository()Task{do{let user =tryawait repository.getUser(id:"123")let success =tryawait repository.saveUser(user: user)print("Save successful: \(success)")}catch{print("Error: \(error)")}}
Kotlin exceptions are automatically converted to Swift errors. However, you should be careful about the types of exceptions you throw, as not all Kotlin exceptions have direct Swift equivalents:
// Kotlin code with custom exceptions
class NetworkException(message: String) : Exception(message)
class ApiService {
suspend fun fetchData(): String {
if (networkUnavailable) {
throw NetworkException("Network is unavailable")
}
return "data"
}
}
// Swift usage with error handlinglet apiService =ApiService()Task{do{let data =tryawait apiService.fetchData()print("Data: \(data)")}catch{// Custom exceptions from Kotlin arrive as KotlinException/NSErrorprint("Error: \(error.localizedDescription)")}}
Converting Flow to AsyncSequence
Kotlin Flow can be converted to Swift's AsyncSequence for streaming data. Nowadays there are solid libraries that help with this conversion, such as:
I covered in a separate article how to convert Kotlin Flow to a callback-based API. Basically you can define a wrapper function or class that collects the flow and calls a callback with each emitted value.
extensionFlowWrapper{funcupdates()->AsyncStream<DataUpdate>{AsyncStream{ continuation inlet job =self.observeUpdates( onEach:{ update in continuation.yield(update)}, onError:{ error in continuation.finish(throwing: error)}, onCompletion:{ continuation.finish()}) continuation.onTermination ={_in job.cancel()}}}}
Here is an example view model wrapper:
class NewsViewModel(
val getNews: GetNewsUseCase,
): BaseViewModel() {
private val _state = MutableStateFlow<NewsState>(NewsState.Loading)
val state: StateFlow<NewsState> = _state.asStateFlow()
init {
viewModelScope.launch {
try {
val news = getNews()
_state.value = NewsState.Success(news)
} catch (e: Throwable) {
_state.value = NewsState.Error(e)
}
}
}
}
class NewsViewModelJs(
viewModel: NewsViewModel,
): JsViewModel(viewModel) {
val state: FlowWrapper<NewsState> = FlowWrapper(viewModel.state, viewModel.viewModelScope)
}
Usage in Swift:
let dataStream = viewModel.state.updates()Task{do{fortryawait update in dataStream {print("Received update: \(update.message)")}}catch{print("Stream error: \(error)")}}
Using Swift Async Functions from Kotlin
When you need to call Swift async functions from Kotlin code, you need to bridge between the two concurrency models. This typically involves creating wrapper functions that convert Swift's async/await to Kotlin's suspending functions.
Creating Suspending Wrappers
You can create Kotlin suspending functions that call Swift async functions using continuation-based approach:
// Kotlin wrapper for Swift async function
expect suspend fun callSwiftAsyncFunction(parameter: String): String
// iOS implementation
actual suspend fun callSwiftAsyncFunction(parameter: String): String =
suspendCancellableCoroutine { continuation ->
// This would typically be implemented in the iOS source set
// using platform-specific code to call Swift async functions
SwiftAsyncBridge.callAsyncFunction(parameter) { result, error ->
if (error != null) {
continuation.resumeWithException(Exception(error.localizedDescription))
} else {
continuation.resume(result ?: "")
}
}
}
Handling Swift Tasks in Kotlin
When working with Swift Tasks from Kotlin, you need to be careful about cancellation and lifecycle management:
// Kotlin class that manages Swift Tasks
class SwiftTaskManager {
private val activeTasks = mutableSetOf<SwiftTask>()
suspend fun executeSwiftTask(operation: String): String = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
val task = SwiftTaskBridge.createTask(operation) { result, error ->
activeTasks.remove(task)
if (error != null) {
continuation.resumeWithException(Exception(error.localizedDescription))
} else {
continuation.resume(result ?: "")
}
}
activeTasks.add(task)
continuation.invokeOnCancellation {
task.cancel()
activeTasks.remove(task)
}
}
}
fun cancelAllTasks() {
activeTasks.forEach { it.cancel() }
activeTasks.clear()
}
}
Conclusion
In this article, we explored how to bridge Kotlin Coroutines and Swift's async/await model. We covered how to convert suspending functions to Swift async functions, handle exceptions, and adapt Kotlin Flow to Swift's AsyncSequence. We also looked at how to call Swift async functions from Kotlin code and manage Swift Tasks in a Kotlin-friendly way.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.