article banner

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 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 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.