| name | ios-swift-to-kmp |
| description | Introducing Kotlin Multiplatform into an existing Swift iOS app — wrapping Swift domain code, packaging the shared framework, and bridging Kotlin `Flow`/suspend to Swift. Use when adding KMP to a shipping iOS app. |
Migrating an iOS Swift App to KMP
Instructions
Unlike Android, iOS teams usually adopt KMP to share logic with Android, not to replace Swift. The shared module is a dependency the iOS app consumes as an XCFramework. SwiftUI stays SwiftUI.
1. Create a KMP module alongside the Xcode project
my-ios-app/
├── iosApp.xcodeproj
├── iosApp/ # existing Swift sources
└── kmp/
├── settings.gradle.kts
├── build.gradle.kts
└── shared/
└── src/…
kmp/shared/build.gradle.kts declares iOS targets only at first:
kotlin {
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach {
it.binaries.framework { baseName = "Shared"; isStatic = true }
}
sourceSets.commonMain.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.darwin)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
}
2. Package as XCFramework (recommended) or CocoaPods
XCFramework via a Gradle task:
val xcf = XCFramework("Shared")
kotlin {
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach {
it.binaries.framework {
baseName = "Shared"; isStatic = true
xcf.add(this)
}
}
}
Build with ./gradlew :shared:assembleSharedXCFramework and drop build/XCFrameworks/release/Shared.xcframework into the Xcode project (or publish via Swift Package Manager).
CocoaPods alternative (for teams already on pods):
plugins { alias(libs.plugins.kotlinCocoapods) }
kotlin {
cocoapods {
summary = "Shared KMP module"
homepage = "https://example.com"
ios.deploymentTarget = "14.0"
framework { baseName = "Shared"; isStatic = true }
}
}
3. Share the domain, not the UI
Move pure Swift domain logic (pricing, feature flags, DTO parsing) into commonMain as Kotlin. Keep SwiftUI views untouched. The shared module exposes ViewModel / Store / Controller types that Swift views observe.
class FeedStore(private val repo: ArticleRepository) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val _state = MutableStateFlow(FeedState())
val state: StateFlow<FeedState> = _state.asStateFlow()
init { scope.launch { repo.observeLatest().collect { _state.value = FeedState(it) } } }
fun refresh() = scope.launch { repo.refresh() }
fun onCleared() { scope.cancel() }
}
4. Consuming suspend and Flow from Swift
Without helpers, a suspend fun foo(): String appears in Swift as:
foo(completionHandler: (String?, Error?) -> Void)
Flow<T> is worse — it exposes raw FlowCollector. Use SKIE to get idiomatic Swift:
plugins { alias(libs.plugins.skie) }
With SKIE, suspend becomes Swift async throws, and Flow<T> becomes AsyncSequence:
import Shared
@MainActor
final class FeedObservable: ObservableObject {
@Published private(set) var state = FeedState(articles: [])
private let store: FeedStore
private var task: Task<Void, Never>?
init(store: FeedStore) {
self.store = store
task = Task { [weak self] in
for await s in store.state {
guard let self else { return }
self.state = s
}
}
}
deinit { task?.cancel(); store.onCleared() }
}
5. Without SKIE — manual adapter
class FlowWrapper<T>(private val flow: Flow<T>) {
fun subscribe(
onEach: (T) -> Unit,
onComplete: () -> Unit,
onError: (Throwable) -> Unit,
): Cancellable {
val job = CoroutineScope(Dispatchers.Main).launch {
try { flow.collect { onEach(it) }; onComplete() } catch (e: Throwable) { onError(e) }
}
return Cancellable { job.cancel() }
}
}
class Cancellable(private val cancel: () -> Unit) { fun cancel() = cancel() }
fun <T> Flow<T>.wrap() = FlowWrapper(this)
6. Memory, threading, and deinit
- New memory model (default) — you can freely share immutable state from background coroutines to the main thread.
- Always cancel the
CoroutineScope the KMP object created in Swift's deinit / onDisappear, or the coroutines keep running.
Dispatchers.Main.immediate dispatches on the UIKit main loop; do not do heavy work on it.
7. What to migrate first
Good first candidates: network DTOs + mappers, pricing/formatting, feature flag evaluation, analytics envelope construction. Avoid: UIKit/SwiftUI glue, Keychain access, push-notification handling — keep in Swift.
Checklist