# 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.

- URL: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-06-run-app-on-ios
- Course: [Mobile Action Pro](https://dantech.academy/en/courses/kotlin-accelerator.md)
- Module: Setup App Running
- Duration: 20 min
- Difficulty: beginner
- Access: Free, full lesson
- Tiếng Việt: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-06-run-app-on-ios.md

## Objectives

- 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

---

## 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

| Requirement                | Notes                                                                                                                          |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| A Mac with Apple Silicon   | The KMP modules only declare the `iosArm64()` and `iosSimulatorArm64()` targets, so the Simulator on an Intel Mac can't run it |
| Xcode                      | Get it from the [Mac App Store](https://apps.apple.com/app/xcode/id497799835). Open it once so it installs extra components and the iOS Simulator |
| JDK 17                     | The same JDK 17 you use for Android. Xcode calls Gradle to build the Kotlin part                                               |
| A working Android build    | Android 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](https://kotlinlang.org/docs/multiplatform/multiplatform-ios-integration-overview.html).

## 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:

| Key                             | Value                                                                                 |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| `SUPABASE_URL`                  | Your Supabase Project URL, written as `https:$(SLASH)$(SLASH)your-project.supabase.co` |
| `SUPABASE_PUBLISHABLE_KEY`      | The same publishable key (`sb_publishable_...`) you use in `local.properties`         |
| `SUPABASE_ANON_KEY`             | Leave empty. It's a temporary fallback, only read when `SUPABASE_PUBLISHABLE_KEY` is empty |
| `GOOGLE_OAUTH_CLIENT_ID`        | The web client ID, the same value as Android's `google.server.clientId`               |
| `GOOGLE_IOS_CLIENT_ID`          | A separate iOS OAuth client. Can stay empty if you haven't done Google Sign-In yet    |
| `GOOGLE_IOS_CLIENT_ID_REVERSED` | The iOS client ID written in reverse, in the form `com.googleusercontent.apps.<id>`   |
| `PRODUCT_BUNDLE_IDENTIFIER`     | Keep `com.creative.kta`                                                               |
| `DEVELOPMENT_TEAM`              | Leave empty for the Simulator                                                         |
| `REVENUECAT_API_KEY`            | RevenueCat's `appl_` key. Don't use Android's `goog_` key                             |
| `PREMIUM_GATE_ENABLED`          | Keep `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

| Feature          | Android                              | iOS                                                                |
| ---------------- | ------------------------------------ | ------------------------------------------------------------------ |
| AI hint          | Gemini if Remote Config has a key, otherwise local hints | Always local hints. iOS isn't wired to Remote Config yet |
| Analytics        | Firebase Analytics                   | An empty implementation (`NoOpAnalyticsManager`)                   |
| Daily challenge  | Follows the `daily.challenge.enabled` flag | Always 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

| Error                                                            | Check 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=17`         | The machine has no JDK 17. Install JDK 17                                                                                              |
| `Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56`   | `KmpLibraryConventionPlugin.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.xcconfig`                       | You haven't created `iosApp/Configuration/Config.xcconfig` yet (Part 2)                                                                |
| Email login works on Android but fails on iOS                    | Check whether `SUPABASE_URL` got cut off at `//`. Use `$(SLASH)$(SLASH)`                                                               |
| Errors with `generic/platform=iOS Simulator`                     | Use a destination with a specific Simulator name (Part 4)                                                                              |
| Google Sign-In opens the browser and never comes back to the app | `GOOGLE_IOS_CLIENT_ID_REVERSED` doesn't match `GOOGLE_IOS_CLIENT_ID`. Read the error log from `checkGoogleSignInConfiguration`          |
| Signing error on a real iPhone                                   | Set `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`.

---

- Previous lesson: [First Build, Test And Commit](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-05-first-build-test-commit.md)
- Next lesson: [Read The Module Graph](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-01-read-module-graph.md)
