Dan Tech Academy
After this lesson you will
  • Understand how Kotlin code gets into the iOS app through the Shared framework and an Xcode build phase
  • Create Config.xcconfig from the template and follow the config path from xcconfig to AppBuildConfig
  • Run the app on the iOS Simulator from Xcode and from xcodebuild, and fix the common setup errors
Duration20 min
DifficultyBeginner

Run The App On iOS (macOS + Xcode)

Run the same Kotlin Multiplatform app on the iOS Simulator with Xcode. Optional lesson; needs a Mac with Apple Silicon.

Video in production

Lesson video is on the way

While you wait, read the full text version below: complete theory, code samples and the hands-on checklist.

Recording is in post-production

Lesson Goal

By the end of this lesson you can run the same course app on the iOS Simulator, from Xcode and from the terminal, and you know the path Kotlin code takes into the iOS app.

This lesson is optional and needs a Mac. If you use Windows or Linux, skip it: everything else in the course works on Android.

What You Need

RequirementNotes
A Mac with Apple SiliconThe KMP modules only declare the iosArm64() and iosSimulatorArm64() targets, so the Simulator on an Intel Mac can't run it
XcodeGet it from the Mac App Store. Open it once so it installs extra components and the iOS Simulator
JDK 17The same JDK 17 you use for Android. Xcode calls Gradle to build the Kotlin part
A working Android buildAndroid builds and tests pass first, so iOS errors don't get mixed up with general setup errors

Running on the Simulator doesn't need the Apple Developer Program. Running on a real iPhone needs an Apple ID and a team in Xcode, covered at the end of this lesson.

Part 1: How Kotlin Gets Into The iOS App

On Android, :app is a Gradle module that depends directly on the Kotlin code. iOS can't read Gradle modules. Xcode only links against frameworks.

So the :shared-app module builds all the shared Kotlin code (the features, :core, :data, :domain...) into a static framework called Shared. The Xcode project in iosApp/ has a build phase called "Build Shared framework" that runs:

bash
./gradlew :shared-app:embedAndSignAppleFrameworkForXcode

The Swift side has just two small files:

  • iosApp/iosApp/iOSApp.swift calls SharedKoinKt.doInitApp() when the app starts.
  • iosApp/iosApp/ContentView.swift wraps MainViewControllerKt.MainViewController(), a UIViewController that holds the whole Compose Multiplatform UI.

Every screen you see on iOS is Kotlin code in commonMain, exactly as on Android.

This model (one Kotlin framework, a thin Swift app shell, Xcode calling Gradle in a build phase) is the common way KMP projects integrate. Reference: iOS integration methods.

Part 2: Create Config.xcconfig

Android reads the Supabase URL, keys and flags from local.properties. iOS has no BuildConfig, so the values take a different route:

text
iosApp/Configuration/Config.xcconfig
-> Info.plist (the $(SUPABASE_URL), $(SUPABASE_PUBLISHABLE_KEY)... entries)
-> iosAppBuildConfig() in shared-app/src/iosMain/.../SharedKoin.kt
-> AppBuildConfig, the same data class Android fills from BuildConfig

Create the file from the template at the repo root:

bash
cp iosApp/Configuration/Config.xcconfig.example iosApp/Configuration/Config.xcconfig

Config.xcconfig is already in .gitignore. Open it and fill in:

KeyValue
SUPABASE_URLYour Supabase Project URL, written as https:$(SLASH)$(SLASH)your-project.supabase.co
SUPABASE_PUBLISHABLE_KEYThe same publishable key (sb_publishable_...) you use in local.properties
SUPABASE_ANON_KEYLeave empty. It's a temporary fallback, only read when SUPABASE_PUBLISHABLE_KEY is empty
GOOGLE_OAUTH_CLIENT_IDThe web client ID, the same value as Android's google.server.clientId
GOOGLE_IOS_CLIENT_IDA separate iOS OAuth client. Can stay empty if you haven't done Google Sign-In yet
GOOGLE_IOS_CLIENT_ID_REVERSEDThe iOS client ID written in reverse, in the form com.googleusercontent.apps.<id>
PRODUCT_BUNDLE_IDENTIFIERKeep com.creative.kta
DEVELOPMENT_TEAMLeave empty for the Simulator
REVENUECAT_API_KEYRevenueCat's appl_ key. Don't use Android's goog_ key
PREMIUM_GATE_ENABLEDKeep false

Why the SLASH = / line? In an xcconfig file, // starts a comment. If you paste the full URL as is, the value gets cut off right after https:. The template defines SLASH separately and joins the pieces.

The PREMIUM_GATE_ENABLED flag only turns on when the value is exactly true. A typo leaves the paid features off, which is the safe direction.

Google Sign-In On iOS

Google won't let a web client redirect to an app's custom URL scheme, so iOS needs its own iOS OAuth client in the Google Cloud Console, created with the bundle ID com.creative.kta. Once it exists:

  1. Put the client ID in GOOGLE_IOS_CLIENT_ID and the reversed form in GOOGLE_IOS_CLIENT_ID_REVERSED. Info.plist declares the reversed form in CFBundleURLSchemes so Google can redirect back to the app.
  2. Add this iOS client ID to the Google provider in Supabase Auth, or Supabase rejects the token for the wrong audience.

At startup, the checkGoogleSignInConfiguration function logs a warning if GOOGLE_IOS_CLIENT_ID is empty, and logs an error if the URL scheme in Info.plist doesn't match the client ID.

Part 3: Run From Xcode

  1. Open iosApp/iosApp.xcodeproj in Xcode.
  2. Pick the iosApp scheme and an iPhone Simulator in the toolbar.
  3. Click Run (Cmd + R).

The first build is very slow. Gradle downloads the Kotlin/Native toolchain and compiles every module for iOS. Later builds are much faster thanks to caching. Follow progress in Xcode's Report navigator, under the "Build Shared framework" build phase.

The app opens on the same Splash and Menu screens as Android, including every string you changed in commonMain.

Part 4: Build From The Terminal

bash
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 16' build

Replace iPhone 16 with the name of a Simulator on your machine (xcrun simctl list devices available lists them).

Don't use -destination 'generic/platform=iOS Simulator'. A generic destination also asks for the x86_64 architecture, which no module declares as a target, and the Compose resources sync step fails.

Part 5: Run The Tests On iOS

On a Mac, each KMP module's allTests runs the tests on both the JVM and the iOS Simulator:

bash
./gradlew :core:iosSimulatorArm64Test
./gradlew :feature:game:allTests

commonTest runs twice, once on the JVM and once on Kotlin/Native. A test can pass on the JVM and fail on iOS, for example when shared code accidentally relies on behavior only the JVM has. So a green Android run doesn't mean iOS is green.

Differences You Will See On iOS

FeatureAndroidiOS
AI hintGemini if Remote Config has a key, otherwise local hintsAlways local hints. iOS isn't wired to Remote Config yet
AnalyticsFirebase AnalyticsAn empty implementation (NoOpAnalyticsManager)
Daily challengeFollows the daily.challenge.enabled flagAlways off

These differences live in each platform's Koin bindings (DataModule.ios.kt versus DataModule.android.kt). The shared UI has no if (platform) branches.

Troubleshooting Table

ErrorCheck first
The "Build Shared framework" build phase can't find Java (for example Unable to locate a Java Runtime)Xcode build phases don't read ~/.zshrc, so a JAVA_HOME set there has no effect. Install JDK 17 with the .pkg file or brew install --cask temurin@17 so the JDK sits in /Library/Java/JavaVirtualMachines
Cannot find a Java installation ... languageVersion=17The machine has no JDK 17. Install JDK 17
Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56KmpLibraryConventionPlugin.linkSwiftRuntimeShims uses xcode-select -p to find the Swift libraries. Run xcode-select -p and confirm it points at the Xcode app, not the Command Line Tools
Xcode says it can't open Config.xcconfigYou haven't created iosApp/Configuration/Config.xcconfig yet (Part 2)
Email login works on Android but fails on iOSCheck whether SUPABASE_URL got cut off at //. Use $(SLASH)$(SLASH)
Errors with generic/platform=iOS SimulatorUse a destination with a specific Simulator name (Part 4)
Google Sign-In opens the browser and never comes back to the appGOOGLE_IOS_CLIENT_ID_REVERSED doesn't match GOOGLE_IOS_CLIENT_ID. Read the error log from checkGoogleSignInConfiguration
Signing error on a real iPhoneSet DEVELOPMENT_TEAM to the team ID your Apple ID on this Mac belongs to

Checkpoint

You are done with this lesson when:

  • The app runs on the iOS Simulator and reaches the Menu,
  • Splash and Menu look the same as on Android,
  • The xcodebuild command in Part 4 ends with BUILD SUCCEEDED,
  • git status doesn't list iosApp/Configuration/Config.xcconfig.
Sign in