Demo project for the KMP Bits article on Swift Export. Shows three things Swift Export in Kotlin 2.3.20 handles that the old Objective-C bridge didn't: exhaustive enum switches, typed sealed class properties, and variadic functions.
Android — Jetpack Compose
iOS — SwiftUI
Shared logic — Kotlin Multiplatform (shared module)
A Kotlin enum class Status { Loading, Success, Error } exported as a real Swift enum. The SwiftUI switch is exhaustive with no default case. Add a new case in Kotlin and Swift breaks at compile time.
A WeatherState sealed class with Loading, Success, and Error subclasses. Properties (temperature, condition, message) arrive typed and named correctly on the Swift side — no adapter, no manual unwrapping. Sealed classes export as an open class hierarchy, so the switch isn't exhaustive, but the data is clean.
A shared Logger object with a vararg messages: String parameter. Swift Export maps it to native variadic syntax: Logger.shared.log(tag: "Demo", messages: "a", "b", "c") instead of an array literal.
shared/
commonMain/
Status.kt — enum class
WeatherState.kt — sealed class
WeatherViewModel.kt — plain class, no framework dependency
Logger.kt — object with vararg fun
composeApp/
androidMain/
App.kt — Compose UI (Enum, Sealed, Vararg screens)
WeatherAndroidViewModel.kt — ViewModel wrapper for Android
iosApp/
iosApp/
ContentView.swift — TabView, EnumDemoView, LoggerDemoView
WeatherView.swift — WeatherViewModelWrapper + WeatherView
WeatherViewModel in shared is a plain class with no ViewModel inheritance. Swift Export exports transitive dependencies — if you add lifecycle-viewmodel to commonMain, it tries to export the entire lifecycle library and fails on generic APIs it can't handle. The ViewModel wrapper lives in composeApp (Android) and in Swift (iOS) instead.
- Android Studio Meerkat or later
- Xcode 16+
- Kotlin 2.3.20
- JDK 17 (Android Studio's bundled JDK — see iOS note below)
Open in Android Studio and run composeApp on a device or emulator.
Or from the terminal (use Android Studio's JDK to avoid AGP/JDK incompatibilities):
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
./gradlew :composeApp:assembleDebugOpen iosApp/iosApp.xcodeproj in Xcode and run on a simulator or device.
The Xcode build phase script calls embedSwiftExportForXcode and pins JAVA_HOME to Android Studio's bundled JDK. This is required — Xcode uses the system Java by default, and JDK 17+ versions that AGP doesn't support will cause the Gradle task to fail silently, producing a "No such module 'Shared'" error in Swift compilation.
The build phase script:
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
cd "$SRCROOT/.."
./gradlew :shared:embedSwiftExportForXcodeTwo changes from a standard KMP setup:
1. shared/build.gradle.kts — replace binaries.framework {} with the swiftExport {} block:
import org.jetbrains.kotlin.gradle.swiftexport.ExperimentalSwiftExportDsl
kotlin {
iosArm64()
iosSimulatorArm64()
@OptIn(ExperimentalSwiftExportDsl::class)
swiftExport {
moduleName = "Shared"
flattenPackage = "com.yourapp.shared"
}
}The @OptIn is required. Without it the block is silently ignored and the task never registers.
2. Xcode build phase — replace embedAndSignAppleFrameworkForXcode with embedSwiftExportForXcode (with the JAVA_HOME pin above).
- Flows (
StateFlow,SharedFlow) are not yet part of Swift Export's stable output. Use KMP-NativeCoroutines or SKIE for async types. - Sealed classes export as open class hierarchies in Swift — no exhaustive
switch. - Enum cases keep Kotlin capitalization:
.Loading, not.loading. If you're migrating from SKIE (which lowercases them), call sites will break. - Swift Export is still experimental. Check the official docs for current limitations.