Ios swift to kmp
Skill almasumdev/awesome-kotlin-multiplatform-agent-skills/.github/skills/migration/ios-swift-to-kmp
Curated agent skills, conventions, and workflows for building Kotlin Multiplatform (KMP) apps with AI coding agents.
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill ios-swift-to-kmpAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
5.5 KB, as published. Nobody here has run it
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:
// shared/build.gradle.kts
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.
// commonMain
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 { // SKIE-generated AsyncSequence
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
CoroutineScopethe KMP object created in Swift'sdeinit/onDisappear, or the coroutines keep running. Dispatchers.Main.immediatedispatches 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
- Shared framework builds as XCFramework and is consumed via SPM or manual embed.
- SwiftUI views do not import Kotlin-generated types deeper than view-model facades.
- SKIE or a hand-written adapter is in place — no raw
FlowCollectorappears in Swift. - Every KMP object created from Swift has a
deinit/onDisappearthat cancels its scope. - The Kotlin side uses
Dispatchers.Main.immediatefor UI-bound updates. - Xcode project depends on one copy of the framework — no mix of XCFramework + CocoaPods.