# iOS Host And Swift Interop

> How Kotlin becomes an iOS framework, what Swift sees through the Objective-C header, which features are lost crossing the bridge and how Kotlin calls Apple's APIs.

- URL: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-shared-kmp-foundations/lesson-03-ios-host-swift-interop
- Course: [Mobile Action Pro](https://dantech.academy/en/courses/kotlin-accelerator.md)
- Module: Kotlin Multiplatform Foundations
- Duration: 18 min
- Difficulty: beginner
- Access: Free, full lesson
- Tiếng Việt: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-shared-kmp-foundations/lesson-03-ios-host-swift-interop.md

## Objectives

- Explain how the Shared framework is built and wired into Xcode, and what its header contains
- Know the interop limits through Objective-C for suspend, Flow, generics, sealed types, enums, default parameters and exceptions, and when you need SKIE or Swift export
- Call Apple APIs from iosMain and understand the basics of the Kotlin/Native memory model when working with ARC

---

## Lesson Goal

This lesson explains the machinery behind running a KMP app on iOS: how Kotlin becomes a framework, what Swift sees of that framework, which Kotlin features get lost crossing the bridge, and how Kotlin calls Apple's APIs.

After this lesson, you can design the boundary between Kotlin and Swift for a KMP project: what Swift should see, what to hide, and when you need tools like SKIE or Swift export.

The steps in this lesson that need a Mac (inspecting the header, running Xcode) were not run on a Mac when it was written. Every detail about the repo comes from reading the code.

## Part 1: Kotlin Becomes A Framework

Open `shared-app/build.gradle.kts`. For each iOS target, the module declares a framework:

```kotlin
iosTarget.binaries.framework {
    baseName = "Shared"
    isStatic = true
}
```

- `baseName = "Shared"` is the name Swift uses in `import Shared`.
- `isStatic = true` produces a static framework. A comment explains why: a dynamic framework costs app launch time and gives nothing back, because nothing else links against it.

Xcode calls Gradle through the `embedAndSignAppleFrameworkForXcode` build phase. This way of wiring things is called direct integration. Other projects may use CocoaPods, or build an XCFramework and distribute it through Swift Package Manager when the iOS team works in a separate repository. When you meet an unfamiliar project, open Build Phases in Xcode to see which approach it uses.

Docs: [iOS integration methods](https://kotlinlang.org/docs/multiplatform/multiplatform-ios-integration-overview.html), [Build final native binaries](https://kotlinlang.org/docs/multiplatform/multiplatform-build-native-binaries.html).

## Part 2: What Swift Sees

Swift doesn't read Kotlin code directly. The Kotlin/Native compiler generates an Objective-C header for the framework, and Swift imports that header. Everything Swift sees has passed through Objective-C.

```mermaid
%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "primaryBorderColor": "#475569", "secondaryColor": "#1e293b", "tertiaryColor": "#0f172a", "lineColor": "#64748b", "textColor": "#e2e8f0", "clusterBkg": "#0f172a", "clusterBorder": "#475569", "titleColor": "#cbd5e1", "edgeLabelBackground": "#334155", "fontSize": "15px"}, "flowchart": {"wrappingWidth": 420}}}%%
flowchart TB
    classDef ui fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#e2e8f0
    classDef vm fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef dom fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#e2e8f0
    classDef data fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#e2e8f0
    classDef root fill:#1e293b,stroke:#94a3b8,stroke-width:2px,color:#e2e8f0
    classDef bad fill:#1e293b,stroke:#ef4444,stroke-width:2px,color:#e2e8f0
    classDef good fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef note fill:#0f172a,stroke:#475569,stroke-dasharray:4 3,color:#cbd5e1
    accTitle: How Kotlin gets into the iOS app
    accDescr: Kotlin/Native compiles the shared-app Kotlin code for iosArm64 and iosSimulatorArm64 into the static framework Shared, together with an Objective-C header. Xcode calls Gradle through the embedAndSignAppleFrameworkForXcode build phase. Swift imports Shared and calls only two functions: SharedKoinKt.doInitApp in iOSApp.swift and MainViewControllerKt.MainViewController in ContentView.swift.

    K[":shared-app<br/>commonMain + iosMain"]:::vm
    K -->|"Kotlin/Native<br/>iosArm64, iosSimulatorArm64"| F["Shared.framework (static)<br/>+ Objective-C header"]:::dom
    X["Xcode build phase<br/>embedAndSignAppleFrameworkForXcode"]:::root -->|"calls Gradle, embeds the framework"| F
    F -->|"import Shared"| SW["Swift"]:::ui
    SW --> C1["iOSApp.swift<br/>SharedKoinKt.doInitApp()"]:::ui
    SW --> C2["ContentView.swift<br/>MainViewControllerKt.MainViewController()"]:::ui
    C2 --> C3["ComposeUIViewController { AppShell() }"]:::vm
    C1 ~~~ N["The header has only the public API of :shared-app (no export(...)).<br/>A top-level function becomes a class named after the file + Kt."]:::note
```

A few translation rules you will meet right in the repo:

| Kotlin                                      | What Swift sees                                    |
| ------------------------------------------- | -------------------------------------------------- |
| The top-level function `doInitApp()` in `SharedKoin.kt` | `SharedKoinKt.doInitApp()`             |
| The top-level function `MainViewController()` in `MainViewController.kt` | `MainViewControllerKt.MainViewController()` |
| Classes, interfaces                         | Classes, protocols, with a prefix taken from the framework name in Objective-C |
| `object`                                    | A class with a `shared` property                   |
| `private`, `internal`                       | Not visible                                        |

Top-level functions are grouped into a class named after the file plus a `Kt` suffix. That's why the KDoc of `doInitApp` points out that the file is named `SharedKoin.kt` so that the class in the header is `SharedKoinKt`. Rename the Kotlin file and you have to fix the Swift too.

The function is named `doInitApp` instead of `initApp`. In Objective-C, methods that start with `init` belong to the initializer family and are treated differently from ordinary methods. The KDoc explains that the repo chose to avoid that prefix entirely.

### What The Header Contains

According to the docs, the framework by default contains the public API of the module being built. A dependency module's API only goes into the header when you call `export(...)` in the `framework` block and declare that dependency with `api(...)`. The docs advise against turning on `transitiveExport`, because it pulls in indirect dependencies too, which increases build time and binary size.

`:shared-app` doesn't call `export(...)` at all. A comment at the top of the file states the intent: only what is really needed in the header gets exported.

A code-reading exercise: find the public top-level declarations in `shared-app/src/commonMain` and `shared-app/src/iosMain`. You will see `AppShell`, `appKoinModules`, `SplashViewModel`, `IosNotifier`... Swift only calls two functions. The rest is still public, so it still goes into the header. To hide a declaration from Swift while still letting other Kotlin modules use it, the docs offer `@HiddenFromObjC`. If only that module uses it, `internal` is enough.

Docs: [Interoperability with Swift/Objective-C](https://kotlinlang.org/docs/native-objc-interop.html).

## Part 3: What Gets Lost Going Through Objective-C

Objective-C is much older than Kotlin and Swift, so many features have no complete translation. This is the table to keep in mind when you design an API for Swift to call:

| Kotlin feature             | What Swift gets                                                         |
| -------------------------- | ----------------------------------------------------------------------- |
| `suspend fun`              | A function with a completion handler. You can call it with `async` in Swift, but the docs say this is still highly experimental |
| `Flow`                     | An ordinary Kotlin interface that you collect through a callback. The element type is lost |
| Generics                   | Only kept on classes. Generics on interfaces and functions are lost     |
| `sealed class`/`interface` | An ordinary class tree. A Swift `switch` needs a `default`              |
| `enum class`               | A class, not a Swift enum                                               |
| Parameters with default values | Swift has to pass every parameter                                   |
| Exceptions                 | A function without `@Throws` that throws into Swift stops the app completely |
| `Int?`                     | Boxed into an object such as `KotlinInt`                                |

Exceptions are the most dangerous row. Kotlin has no checked exceptions; Swift does. The docs are explicit: an exception that reaches Swift and isn't in the list declared by `@Throws` is treated as unhandled and stops the program. For a `suspend fun` without `@Throws`, only `CancellationException` is converted to an `NSError`.

The repo sidesteps this whole table by design: Swift only calls two functions with no parameters, one that returns nothing and one that returns a `UIViewController`. The whole UI is written in Compose Multiplatform in Kotlin, so no ViewModel, `Flow` or `sealed class` has to cross the bridge. The thinner the Swift shell, the less this table matters.

If you take over a project whose iOS UI is written in SwiftUI and calls Kotlin ViewModels, this table touches every screen. That's when you need Part 4.

Docs: the [Kotlin-Swift interopedia](https://github.com/kotlin-hands-on/kotlin-swift-interopedia), maintained by JetBrains, has an example for every row in the table.

## Part 4: SKIE And Swift Export

Two tools address the table in Part 3. The repo uses neither, but you will meet them in other projects.

| Tool           | What it is                                                        | What it fills in                                          | Status                      |
| -------------- | ----------------------------------------------------------------- | --------------------------------------------------------- | --------------------------- |
| SKIE           | A Gradle plugin from Touchlab that generates extra Swift code into the framework | `Flow` as `AsyncSequence`, `suspend` with cancellation, exhaustive `switch` over enums and sealed types, default parameters | A third-party library |
| Swift export   | A feature of Kotlin itself that exports straight to Swift without going through Objective-C | `suspend` as `async`, `Flow` as `AsyncSequence`, sealed types as Swift enums, packages preserved | Alpha according to the Kotlin docs |

Two notes if you consider Swift export: the docs say it only works with direct integration, and generics are erased to their upper bound when exported. Since it's in Alpha, don't put a project that handles real money on it without thorough testing.

Docs: [Swift export](https://kotlinlang.org/docs/native-swift-export.html), [SKIE](https://skie.touchlab.co/).

## Part 5: Kotlin Calling Apple APIs

The other direction is much easier. In `iosMain`, Apple's system frameworks are available under the `platform.*` packages with no extra configuration.

| File in the repo             | Apple API used                                         |
| ---------------------------- | ------------------------------------------------------ |
| `IosDataStoreFileResolver`   | `NSFileManager`, `NSURL` (Foundation)                  |
| `SharedKoin.kt`              | `NSBundle`, to read values from `Info.plist`           |
| `IosShareSheet`              | `UIActivityViewController` (UIKit)                     |
| `IosGoogleSignInLauncher`    | `ASWebAuthenticationSession` (AuthenticationServices)  |
| `GoogleOAuthPkce`            | `CC_SHA256` (CoreCrypto), `SecRandomCopyBytes` (Security) |

Some C-style APIs require `@OptIn(ExperimentalForeignApi::class)`, as you can see on `IosDataStoreFileResolver` and in `GoogleOAuthPkce`.

An important limit: Kotlin can only call Swift when that API is exposed to Objective-C with `@objc`. Pure Swift modules aren't supported yet. The KDoc of `IosGoogleSignInLauncher` shows the consequence: instead of pulling in the Google Sign-In SDK, the repo writes its own OAuth flow with PKCE using Apple's APIs, so it doesn't have to add a seam written in Swift.

Docs: [Platform libraries](https://kotlinlang.org/docs/native-platform-libs.html).

## Part 6: Memory Model Basics

Kotlin/Native uses a tracing garbage collector, like the JVM: objects live on a shared heap and can be accessed from any thread. Swift and Objective-C use ARC, reference counting. The two mostly cooperate smoothly, but there are two points you need to know:

- Retain cycles. Kotlin's GC can reclaim reference cycles between Kotlin objects. ARC can't. According to the docs, if even one Objective-C or Swift object is in the cycle, the whole cycle isn't reclaimed. When Swift passes a closure that captures `self` into Kotlin, use `[weak self]` just as you would in ordinary Swift code.
- Deinit. A Swift object passed into Kotlin on the main thread runs its `deinit` on the main thread, after a GC cycle rather than immediately.

If older docs or articles mention `freeze()` or not being allowed to share objects between threads, that's the old memory manager. Current code doesn't need any of that.

Docs: [Kotlin/Native memory management](https://kotlinlang.org/docs/native-memory-manager.html), [Integration with Swift/Objective-C ARC](https://kotlinlang.org/docs/native-arc-integration.html).

## Part 7: Compose In The iOS App

`MainViewController` wraps `AppShell()` in a `ComposeUIViewController`, which returns an ordinary `UIViewController`. `ContentView.swift` wraps it so SwiftUI can display it. This is the standard pattern the Compose Multiplatform docs describe for embedding Compose in a SwiftUI app.

Config such as the Supabase URL or the RevenueCat key goes from `Config.xcconfig` into `Info.plist`, and Kotlin reads it with `NSBundle` in `iosAppBuildConfig`.

Docs: [Integration with SwiftUI](https://kotlinlang.org/docs/multiplatform/compose-swiftui-integration.html).

## Troubleshooting Table

| Symptom                                                      | Usual cause                                               | Fix                                                                 |
| ------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------- |
| Swift says it can't find `SharedKoinKt`                      | The Kotlin file was renamed, so the class in the header changed with it | Rename the file back or fix the call in Swift          |
| The iOS app stops suddenly when Swift calls a Kotlin function | The function throws an exception without `@Throws`       | Catch the error in Kotlin, or declare `@Throws` so Swift can handle it |
| Swift can't see a type from another module                   | That module isn't `export(...)`ed                         | Add `export(...)` and declare `api(...)`, only for types that are really needed |
| A Swift `switch` over a sealed class demands a `default`     | The sealed class became an ordinary class tree through Objective-C | Accept the `default`, or use SKIE                          |
| iOS app memory keeps growing around one screen               | A Swift closure holds `self` inside a Kotlin object       | Use `[weak self]` in the closure                                    |

## Checkpoint

You are done with this lesson when you can:

- explain why Swift calls `SharedKoinKt.doInitApp()` and not `doInitApp()`,
- say which module's API the `Shared` framework contains, and what you would have to do for Swift to see a type from `:domain`,
- pick the three rows in the Part 3 table that would cause the most trouble if the iOS UI were written in SwiftUI,
- explain why the repo doesn't need SKIE or Swift export,
- name two Apple APIs the repo calls directly from Kotlin.

---

- Previous lesson: [Platform Seams: expect/actual Or Interface + DI](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-shared-kmp-foundations/lesson-02-platform-seams-expect-actual-di.md)
- Next lesson: [The Multiplatform Data Stack](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-shared-kmp-foundations/lesson-04-kmp-data-stack.md)
