
Kotlin Coroutines and Swift
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
Taskfor unstructured concurrency, while Kotlin useslaunchandasync - 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 usessuspendmodifier to mark functions that can suspend the coroutine andlaunchandasyncto start coroutines
// Swift async function func fetchUserData(userId: String) async throws -> User { let response = try await networkService.get("/users/\(userId)") return try JSONDecoder().decode(User.self, from: response) } // Swift Task for unstructured concurrency Task { do { let user = try await fetchUserData(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 interface class UserRepository : KotlinBase { func getUser(id: String) async throws -> User func saveUser(user: User) async throws -> KotlinBoolean } // Usage in Swift let repository = UserRepository() Task { do { let user = try await repository.getUser(id: "123") let success = try await 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 handling let apiService = ApiService() Task { do { let data = try await apiService.fetchData() print("Data: \(data)") } catch { // Custom exceptions from Kotlin arrive as KotlinException/NSError print("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.
class FlowWrapper<T>( private val flow: Flow<T>, private val scope: CoroutineScope, ) { fun subscribe( scope: CoroutineScope, onEach: ((T) -> Unit)? = null, onError: ((Throwable) -> Unit)? = null, onCompletion: (() -> Unit)? = null, ): Cancellable { return flow .let { flow -> onEach?.let { flow.onEach { onEach(it) } } ?: flow } .let { flow -> onCompletion?.let { flow.onCompletion { onCompletion() } } ?: flow } .let { flow -> onError?.let { flow.catch { onError(it) } } ?: flow } .launchIn(scope) .let { job -> object : Cancellable { override fun cancel() { job.cancel() } } } } interface Cancellable { fun cancel() } }
Then in Swift, you can create an AsyncSequence:
extension FlowWrapper { func updates() -> AsyncStream<DataUpdate> { AsyncStream { continuation in let 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 { for try await 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.