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
AuthServiceand oneAuthResultmodel 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
Anycrossing 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.