← All shorts
48Kotlin 1.9+2 min read

Consuming a KMP Module from Swift

Kotlin/Native compiles shared code to a framework Swift calls directly, with suspend functions arriving as completion handlers.

Shared Kotlin logic earns nothing on iOS if Swift needs a hand-maintained bridge to reach it. Kotlin/Native's binaries.framework { } compiles commonMain and iosMain into an Objective-C-compatible framework. Xcode links it like any other dependency.

#The old way

Two login flows. A Swift AuthService calling URLSession directly, and a Kotlin AuthService on Android that already had the token-refresh and error-mapping logic written. Two state machines that had to agree on behavior, with no shared code to enforce it.

#The new way

class AuthService(private val api: AuthApi) {
    suspend fun login(email: String, password: String): AuthResult {
        return api.authenticate(email, password)
    }
}

sealed class AuthResult {
    data class Success(val token: String) : AuthResult()
    data class Failure(val reason: String) : AuthResult()
}
kotlin {
    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
        it.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }
}
let authService = AuthService(api: AuthApiImpl())

authService.login(email: "[email protected]", password: "hunter2") { result, error in
    if let success = result as? AuthResultSuccess {
        print("Token: \(success.token)")
    } else if let failure = result as? AuthResultFailure {
        print("Failed: \(failure.reason)")
    }
}

The Kotlin sealed class AuthResult becomes a class cluster in the generated headers. Its nested subclasses arrive in Swift as AuthResultSuccess and AuthResultFailure.

#Why it matters

  • One AuthService and one AuthResult model instead of two implementations that can disagree on edge cases
  • The generated framework is a normal Xcode dependency, so there are no headers to maintain by hand
  • Swift gets real, checkable types back, not an opaque Any crossing the bridge

#Gotcha

A Kotlin suspend fun surfaces to Swift as a method taking a trailing (T?, Error?) -> Void completion handler. Never as a Combine Publisher. Swift's concurrency bridging can await that completion-handler signature directly, so try await authService.login(email:password:) works out of the box. Combine is another matter: the generated API has no notion of it, so a pipeline still means wrapping the call in a Future yourself.

kmpkotlincross-platform

Related shorts