Running iOS Apps in Kotlin Multiplatform Projects: What JetBrains Got Wrong
Context
We’ve been working on a Kotlin Multiplatform (KMP) project with multiple Compose-based apps. One of the goals was to make each app module fully autonomous, including their iOS counterparts. This required a clean and scalable way to build and launch each iOS app.
The Out-of-the-Box Setup
By default, KMP projects assume a very narrow use case:
A single
iosApp
folder at the rootOne iOS launcher
One shared KMP module
This structure is good for tutorials and small demos. But in any serious, scalable project, it breaks down almost immediately.
Problems We Faced
1. Root-Level iosApp
: A Scaling Nightmare
The default
iosApp/
at the root of the project is fine for a single-app setup.It does not scale if your project has
:composeApp
,:composeChat
,:composeGame
, etc.Moving
iosApp
into a module (likecomposeApp/iosApp
) breaks Android Studio integration.
2. Android Studio Run Config: Null Outputs and No Clarity
The default "Build iOS Application" config in Android Studio:
Assumes root-level
iosApp
Assumes a Kotlin Native executable (not SwiftUI)
Assumes a hardcoded build output in
build/ios/Debug-iphonesimulator/*.app
When these assumptions fail:
You get a silent failure
A
/null.app
is "built"Exit code 0 misleads you into thinking things are fine
3. The ios/
Build Folder Is a Lie
Android Studio creates a
build/ios/
folder to satisfy its own run config.However, Xcode doesn’t use or need this folder.
The real
.app
lives inDerivedData
or a custom output path.Xcode and
simctl
can run your app just fine without anything inbuild/ios/
.
Our Fix
We created a CLI launcher that:
Builds the
.xcodeproj
viaxcodebuild
Parses the
Config.xcconfig
forAPP_NAME
andBUNDLE_ID
Extracts the real
.app
path fromxcodebuild
logsUses
simctl install
andsimctl launch
to run the app
This gives us a modular, repeatable, IDE-independent way to launch any iOS app per module.
What JetBrains Got Wrong
1. Lack of support for multi-app layouts
There is no official way to support multiple iOS apps cleanly. The root-level iosApp
assumption locks the architecture.
2. No configurability in Android Studio’s Run Config
No way to override output paths, entry points, app name, or bundle ID. Completely opaque.
3. Silent failures by default
Instead of logging a warning or failing with a helpful error, the system builds a /null.app
and exits with code 0.
4. Mixing concerns between Kotlin and Swift
Kotlin/Native wants to build an executable, but modern Swift apps use SwiftUI @main
. There is no clear demarcation between "Kotlin business logic" and "Swift launcher" in the default tooling.
What JetBrains Should Do
Allow
iosApp
to be located in a moduleSupport
.xcodeproj
builds explicitlyProvide an official CLI script to launch SwiftUI-based KMP apps
Make Android Studio’s iOS run config fully customizable
Publish sample projects with multi-app structure
Conclusion
We found a clean and scalable way to integrate SwiftUI-based iOS apps inside a KMP multi-app project. But it required completely abandoning the default Android Studio iOS run config and writing our own launcher logic.
If you're struggling with null.app
, hardcoded paths, and broken assumptions — you're not alone. JetBrains gave us the building blocks. But it’s up to us to make it production-ready.
— With love, from the trenches 💚
What KMP does not provide:
No dynamic binding of frameworks in Xcode
You manually manage
ContentView.swift
imports like:
import shared // or sharedChat, or whatever
There is no Gradle-to-Xcode linkage that updates
module.modulemap
paths, umbrella headers, orimport
statements in Swift files.No contextual framework generation per app module
All KMP-native frameworks are treated uniformly.
baseName
is statically configured per Gradle module.Xcode has no idea what the current Gradle context is, or which module you intend to run.
No app module switching logic
There’s no concept of "active KMP app" at the Xcode level.
Everything from the imported framework to resource paths is hardcoded.
This breaks scalability across multiple app modules:
You cannot build
:composeApp
,:composeChatApp
,:composeGameApp
and expect Xcode to switchimport sharedX.framework
dynamically.It essentially defeats the purpose of modular iOS app layering within a multiplatform project, unless each module has its own Xcode target and fully separate Swift frontends.