agentsclimarketplace

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.

Install
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill ios-swift-to-kmp

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

  • 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 FlowCollector appears in Swift.
  • Every KMP object created from Swift has a deinit/onDisappear that cancels its scope.
  • The Kotlin side uses Dispatchers.Main.immediate for UI-bound updates.
  • Xcode project depends on one copy of the framework — no mix of XCFramework + CocoaPods.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.