A demo Kotlin Multiplatform + Compose Multiplatform project that runs on Android, iOS (via Swift Package Manager), and Web (Wasm/JS).
.
├── composeApp/ # KMP module — Android app + iOS framework + Wasm web
│ └── src/
│ ├── commonMain/ # Shared Compose UI + business logic
│ ├── androidMain/ # Android entry point (MainActivity)
│ ├── iosMain/ # ComposeUIViewController bridge for iOS
│ └── wasmJsMain/ # Wasm/JS entry point + index.html
├── iosApp/iosApp/ # SwiftUI shell that hosts the Compose view
├── Package.swift # Local SPM package wrapping the XCFramework
└── gradle/, settings.gradle.kts, … # Standard Gradle scaffolding
The iOS app does not depend on the Gradle/Kotlin output directly. Instead:
- Gradle builds
composeApp/build/XCFrameworks/release/Shared.xcframework. - The root
Package.swiftexposes that XCFramework as a Swift Package calledShared. - Xcode pulls in the SPM package as a local dependency.
./gradlew :composeApp:assembleSharedReleaseXCFramework
# or for faster iterative dev:
./gradlew :composeApp:assembleSharedDebugXCFrameworkThis produces composeApp/build/XCFrameworks/release/Shared.xcframework (or debug/).
If you use the debug variant, edit Package.swift to point at debug/ instead.
The Gradle wrapper is checked in, so the first run will download Gradle 8.10.2
automatically — no system-wide gradle install needed.
The Swift sources live at iosApp/iosApp/, but the .xcodeproj isn't generated yet.
In Xcode:
- File ▸ New ▸ Project ▸ iOS App — name it
iosApp, save insideiosApp/, replacing the existing folder if asked. (Or save elsewhere and move the two.swiftfiles in.) - Delete Xcode's generated
ContentView.swiftandiosAppApp.swift; add the two files already iniosApp/iosApp/instead. - File ▸ Add Package Dependencies… ▸ Add Local… — select the repo root (
/Users/brian/Dev/kmp-spm). Add theSharedproduct to theiosApptarget. - Build & run on a simulator.
If the
Sharedimport fails to resolve, make sure step 1 (assembleShared…XCFramework) ran first — SPM resolves the binary target by reading the file on disk.
| Platform | Command |
|---|---|
| Android | ./gradlew :composeApp:installDebug (or run from Android Studio) |
| iOS | Build & run iosApp from Xcode |
| Web (Wasm) | ./gradlew :composeApp:wasmJsBrowserDevelopmentRun then open the printed URL |
All versions live in gradle/libs.versions.toml. Bumping Kotlin, AGP, or Compose
Multiplatform from a single place reduces compatibility surprises.
- XCFramework path in
Package.swiftis hardcoded torelease/. Switch todebug/for dev iteration (smaller, faster builds), but remember to rebuild whenever shared Kotlin code changes — SPM caches binary targets aggressively. - Compose iOS framework size: a hello-world XCFramework is ~40–60 MB in debug. Release ARM64-only is much smaller.
- Configuration cache is on. If a plugin misbehaves, disable temporarily
with
--no-configuration-cache.