Shared business logic in Kotlin Multiplatform runs smoothly on Android; KMP's iOS integration demands extra setup discipline because Kotlin/Native first produces a framework, wired into the Xcode project as a separate step: linking it to Xcode, reading Swift interop types correctly, and keeping the build chain from breaking are each a task in themselves. The Swift export extensions in Kotlin 2.4.20 — sealed classes mapping to Swift enums, cross-language inheritance, and automatic Package.swift generation — simplify this side noticeably. This article walks through KMP's iOS side, from Xcode integration paths to Swift export's new capabilities.
💡 Pro Tip: Swift export is still in Alpha; in production, leaving Objective-C header generation as the default and starting to try Swift export on new modules lowers your exposure to API breakage.
Table of Contents
- Why iOS Is the Hardest Side of KMP
- Local vs. Remote Setup
- Ways to Link into the Xcode Project
- Automatic Package.swift Generation (Kotlin 2.4.20)
- Kotlin 2.4.20 Swift Export Additions: Sealed → Swift Enum
- What Happens When a Sealed Type Grows
- Cross-Language Inheritance: Contract in Kotlin, Implementation in Swift
- The Suspend and Flow Bridge: A Quick Reminder
- Types to Watch Out for in Interop
- Why the Alpha Label Matters
- Compile-Time Safety: The First Layer of Debugging
- Faster Iteration with Incremental Compilation
- Getting the iOS Team to Buy Into KMP
- Shared UI with Compose Multiplatform: A Brief Context
- FAQ
- How do you link a KMP module into an Xcode project?
- How does a sealed class show up in Kotlin 2.4.20's Swift export?
- Which types cause trouble in Kotlin/Swift interop?
- How do you handle debugging and crash symbolication on KMP's iOS side?
- Should I choose direct integration or CocoaPods?
- Is Swift export ready for production?
- Conclusion
- Sources
Why iOS Is the Hardest Side of KMP
On Android, since Kotlin/JVM is already the native language, the shared module behaves like a direct dependency. On iOS, Kotlin/Native first compiles to a framework (XCFramework), added to the Xcode project as a separate step, with an interop layer kicking in where Swift's type system meets Kotlin's. Per JetBrains' official integration guide, this linking process isn't a single path — it's a handful of options that vary with the project's structure.
Local vs. Remote Setup
In a local setup, the framework is compiled within the same repo and added directly to the Xcode project; in a remote setup, it's distributed through a package registry (e.g., a SwiftPM registry). Because it's compiled in the same repo locally, the Kotlin build becomes part of the iOS build, and the app updates instantly when shared code changes.
In large, multi-module teams, the situation can flip: the shared module lives in a separate repo, gets published under its own version number, and the iOS app consumes that framework like a third-party dependency. This forces more discipline on the framework's API surface, since every change no longer lands "in the same PR" but reaches iOS as a separate version bump — this is where sealed export's compile-time warnings earn their keep, catching cases forgotten during an upgrade.
Ways to Link into the Xcode Project
Kotlin's official docs first split iOS integration into local and remote: the local setup is described in text as two main options (direct integration and CocoaPods), with three local-path headings (direct integration, SwiftPM with a local Swift package, CocoaPods with a local podspec); remote setup has two paths (SwiftPM with XCFrameworks and CocoaPods with XCFrameworks).
The table below compares these three local setup headings. If you're using the Kotlin Multiplatform IDE plugin, direct integration is applied by default.
Method | When it fits | How it's set up | Swift export |
|---|---|---|---|
Direct integration | New project with the KMP IDE plugin, single repo | A New Run Script Phase is added to Xcode Build Phases; the script calls the :shared:embedAndSignAppleFrameworkForXcode task | Works |
CocoaPods | Project already uses Pod dependencies | Added as a CocoaPods dependency via a local podspec | Doesn't work |
SwiftPM (local package) | Teams who want to consume the framework like a local Swift package | Local path dependency via Package.swift | Doesn't work |
In direct integration, this Run Script phase is placed before Compile Sources, and User Script Sandboxing is turned off in Build Settings. That last column carries a critical constraint: the "Current limitations" section of the official Swift export docs states that Swift export only works in projects that link the iOS framework via direct integration. If you're planning to use Swift export, pick your integration method accordingly.
Automatic Package.swift Generation (Kotlin 2.4.20)
This convenience kicks in under a narrow condition: exporting an XCFramework that depends on SwiftPM packages. The official docs say you need to publish the resulting SwiftPM package for such an XCFramework to resolve correctly; to help, the assembleSharedXCFramework Gradle task now generates a Package.swift file to distribute alongside the XCFramework.
In other words, the feature isn't for "distributing via SwiftPM" in general — it's for exporting an XCFramework that carries a SwiftPM dependency.
1# XCFramework + automatic Package.swift generation2./gradlew :shared:assembleSharedXCFrameworkThe generated Package.swift is pushed to a separate Git repository together with the XCFramework's ZIP archive and published as a Swift package; the iOS project resolves the package as a dependency from that repo.
Kotlin 2.4.20 Swift Export Additions: Sealed → Swift Enum
Kotlin 2.4.20's standout Swift export feature is that sealed class/sealed interface hierarchies now map to a Swift enum. That means you can write exhaustive switch statements in Xcode and get full autocomplete. The sealedType() method generated for every sealed type returns enum cases that correspond directly to the subclasses.
1// Kotlin side — shared module2sealed class PaymentState {3 class Idle : PaymentState() {4 override fun toString(): String = "Idle"5 }6 7 data class Processing(val orderId: String) : PaymentState()8 data class Failed(val reason: String) : PaymentState()9}10 11fun currentPaymentState(): PaymentState = PaymentState.Processing("A-1042")1// Swift side — exhaustive switch on the exported enum2let state = currentPaymentState()3let label = switch state.sealedType() {4case let .idle(type):5 "Bekliyor: \(type.value)"6case let .processing(type):7 "İşleniyor: \(type.value.orderId)"8case let .failed(type):9 "Hata: \(type.value.reason)"10}11print(label) // İşleniyor: A-1042One thing to note: the payload of each case in sealedType()'s return value is a wrapper — you access the subclass instance via type.value, not type.orderId directly.
When a new subclass is added, the Swift compiler warns at compile time that the switch is no longer exhaustive — so instead of relying on a default case, you get compile-time safety. This behavior is covered in detail, compared against the Objective-C bridge, in Kotlin 2.4 Swift Export: Is the Obj-C Bridge Going Away in KMP?.
What Happens When a Sealed Type Grows
Adding a new subclass to an existing sealed hierarchy is something the Kotlin compiler already catches on Android. Since Swift export carries this same safety to Xcode, hunting for "forgotten cases" separately on two platforms disappears — when a Kotlin dev adds a new case, the iOS team notices it at the very first build failure, not on the way to the App Store.
Cross-Language Inheritance: Contract in Kotlin, Implementation in Swift
Kotlin 2.4.20 brings cross-language inheritance support to Swift export: define the contract in Kotlin, provide the platform-specific implementation in Swift. The official docs require two pieces: a Kotlin interface and an open base class the Swift implementation subclasses. The docs' example uses this pattern to hide an Apple-only library like CryptoKit behind a Kotlin interface.
1// Kotlin — the interface, the function that accepts it, and the open base class2interface CryptoProvider {3 fun hashMD5(input: String): String4}5 6fun processHash(provider: CryptoProvider, input: String): String =7 provider.hashMD5(input)8 9open class SwiftBase1// Swift — subclass the exported SwiftBase and implement the interface2import CryptoKit3 4final class IosCryptoProvider: SwiftBase, CryptoProvider {5 func hashMD5(input: String) -> String {6 guard let data = input.data(using: .utf8) else { return "failed" }7 return Insecure.MD5.hash(data: data).description8 }9}10 11let provider = IosCryptoProvider()12// Calls the Kotlin function, which calls back into Swift's hashMD5 method13print(processHash(provider: provider, input: "Hello, world!"))The advantage: without platform-specific expect/actual blocks in the shared module, Swift can wire its own native APIs directly into the Kotlin interface.
The Suspend and Flow Bridge: A Quick Reminder
How Kotlin coroutines (suspend functions and Flow) bridge into Swift's async/AsyncSequence world is one of the cornerstones Swift export laid down in 2.4.0. This article focuses on KMP's iOS integration mechanics, so it skips the detail; the core bridging logic was covered in Kotlin 2.4 Swift Export — in short, suspend functions show up as async, and Flow as an AsyncSequence-conformant type.
This article focuses on sealed export and cross-language inheritance because they're new in 2.4.20 and not yet well documented elsewhere; the suspend/Flow bridge, by contrast, is part of the concurrency support Swift export gained graduating to Alpha in 2.4.0 (July 14, 2026), and it's already documented.
Types to Watch Out for in Interop
As of Kotlin 2.4.20, Swift export is officially Alpha; the API surface and type mappings can change between versions. So verifying exported types' behavior in a real Xcode project with every new Kotlin release matters, especially for generics and complex sealed hierarchies.
Checking the "Current limitations" section of the official Swift export docs for precise, up-to-date constraints is the most reliable step before a production decision.
Why the Alpha Label Matters
While an API is Alpha-labeled, its behavior can change even across minor version bumps. That's why testing version upgrades of Swift-export-dependent frameworks in a separate staging build, before production, is a reasonable safety measure.
Compile-Time Safety: The First Layer of Debugging
The cheapest layer of iOS-side debugging in KMP is bugs that never reach runtime at all. Sealed hierarchies turning into exhaustive switches delivers exactly that: when a new case is added, the compiler flags the missing branches on the Swift side, so a "forgotten case" bug gets caught before it ships to the App Store.
This is a safety layer that's far harder to achieve with NSObject subclasses and NS_ENUM in the classic Objective-C bridge.
Faster Iteration with Incremental Compilation
With Kotlin 2.4.20, incremental compilation of klib artifacts moved to Beta, giving a faster feedback loop on native targets without recompiling the whole module for every small change.
1# gradle.properties — enable incremental native compilation2kotlin.incremental.native=trueGetting the iOS Team to Buy Into KMP
A common objection you'll hear from an iOS team is something like, "this isn't my code, I'm just consuming a framework."
The most effective way to close that distance is to make the shared module look like an ordinary Swift Package in Xcode: the auto-generated Package.swift, exhaustive-switch autocomplete, and compile-time errors barely change the iOS dev's daily workflow. Starting small — sharing a single domain model or the network layer — is a reasonable strategy until trust settles in.
- Pilot module: Start by sharing a single domain (e.g., authentication state), not the whole app.
- Visibility: Show the generated types on the Swift side in the IDE and drive home the message that "this is just an ordinary Swift enum."
- Rollback plan: Work out the cost of removing the framework up front and present it transparently to the team.
- Shared glossary: Keep naming on the Kotlin and Swift sides (sealed type names, function signatures) in sync through a single document; naming mismatches after export create confusion in code review.
The most critical moment in this process is usually the first major version upgrade. While Swift export is Alpha, a Kotlin minor release can change the API surface; doing the upgrade on a trial branch first, with a full Xcode CI build, rather than landing it directly on main, prevents the team from feeling "KMP stopped us with a surprise build failure."
Shared UI with Compose Multiplatform: A Brief Context
This article focuses on KMP's business-logic/interop side; sharing the UI layer is Compose Multiplatform's job, as a separate layer. Compose Multiplatform 1.12.0 released on August 25, 2026, bringing small but noticeable iOS improvements, like text-selection handles adapting to the native look and a "Select" item added to text fields' context menu.
Compose Multiplatform stays compatible with the latest Kotlin release and has been fully on the K2 compiler since 1.8.0. Details on Android + iOS production deployment: Compose Multiplatform: Android + iOS Production Deployment 2026; for KMP's general production story: Kotlin Multiplatform 1.1 Stable: Production Case Study 2026.
Worth clarifying: Swift export and the sealed→enum mapping work purely in the shared business-logic layer, independent of the UI.
You can use every interop technique here with fully native SwiftUI screens, without ever touching Compose Multiplatform — the two are complementary, not dependent on each other.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
A practical checklist to go through before moving into KMP's iOS side, designed to make the move to Swift export safe without breaking the build chain.
FAQ
How do you link a KMP module into an Xcode project?
The Kotlin Multiplatform side produces an XCFramework, compiled with the assembleSharedXCFramework Gradle task and added to Xcode via one of three paths: direct integration, CocoaPods with a local podspec, or SwiftPM with a local Swift package. As of 2.4.20, this task also auto-generates a Package.swift file to distribute alongside the XCFramework when it depends on SwiftPM packages.
How does a sealed class show up in Kotlin 2.4.20's Swift export?
Kotlin's sealed class/sealed interface hierarchy is converted into a Swift enum that you can use with an exhaustive switch.
The sealedType() method generated for every sealed type returns cases corresponding to the subclasses; when a new subclass is added, the Swift compiler reports at compile time that the switch is no longer exhaustive.
Which types cause trouble in Kotlin/Swift interop?
Since Swift export is in Alpha status as of Kotlin 2.4.20, type mappings can show behavior changes between versions. The precise, up-to-date limitations are kept in the "Current limitations" section of the official Swift export documentation; you need to recheck that section against the current Kotlin version before making a production decision.
How do you handle debugging and crash symbolication on KMP's iOS side?
There's no verified 2.4.20-specific innovation in this area; but sealed hierarchies turning into exhaustive switches provides an indirect debugging layer by catching many logic bugs at compile time before they ever reach runtime. This heads off "forgotten case" type bugs before they ship to the App Store.
Should I choose direct integration or CocoaPods?
If the project already uses CocoaPods dependencies, CocoaPods integration doesn't require an extra toolchain; if you're setting up a new project and using the Kotlin Multiplatform IDE plugin, direct integration is the default and lower-friction path.
But if you're planning to use Swift export, the choice isn't free: the official documentation states that Swift export only works in projects using direct integration.
Is Swift export ready for production?
The official documentation states that Swift export is in the Alpha stage as of Kotlin 2.4.20; that doesn't mean the API can't be used in production, but it does mean it's at a maturity level that requires testing version upgrades in a staging environment.
Conclusion
Kotlin 2.4.20 simplified KMP's iOS side on three fronts: SwiftPM integration through automatic Package.swift generation, type safety through sealed→enum export, and access to platform-specific APIs through cross-language inheritance.
None of this means the Objective-C bridge has disappeared entirely — Swift export is still Alpha — but it does noticeably cut down daily development friction. Starting with a small pilot module with your team and bringing the auto-generated Package.swift into the CI chain early is the way to make this transition with the fewest surprises.
For comparative details on sealed export versus the Objective-C bridge, see Kotlin 2.4 Swift Export; for the general production story, see Kotlin Multiplatform 1.1 Production Case Study; and if you want to share the UI layer too, see Compose Multiplatform Production Deployment.
For the K2 compiler's impact on the broader Kotlin ecosystem, Kotlin 2.1: K2 Compiler + Context Parameters is useful, and for a cross-platform selection decision, Flutter vs SwiftUI: 3 Years + 60K LOC Production Comparison offers a helpful comparison.
Sources
- What's new in Kotlin 2.4.20 — Swift export, automatic Package.swift, and incremental compilation details
- Kotlin release schedule — tooling release cadence and the 2.5.0 plan
- KMP iOS integration overview — direct, CocoaPods, and SwiftPM paths
- Swift export documentation — Alpha status and limitations
- Compose Multiplatform compatibility and versioning — Kotlin version compatibility and the K2 transition
- Compose Multiplatform 1.12.0 release notes — iOS text selection and context menu fixes
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy
