# Chạy App Trên iOS (macOS + Xcode)

> Chạy cùng app Kotlin Multiplatform trên iOS Simulator bằng Xcode. Bài tùy chọn, cần Mac chip Apple Silicon.

- URL: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-06-run-app-on-ios
- Khóa học: [Mobile Action Pro](https://dantech.academy/courses/kotlin-accelerator.md)
- Module: Setup App Chạy Được
- Thời lượng: 20 phút
- Độ khó: beginner
- Truy cập: Miễn phí, đọc toàn bài
- English: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-06-run-app-on-ios.md

## Mục tiêu

- Hiểu Kotlin code đi vào app iOS qua framework Shared và build phase của Xcode
- Tạo Config.xcconfig từ template và hiểu đường đi của config từ xcconfig tới AppBuildConfig
- Chạy app trên iOS Simulator từ Xcode và từ xcodebuild, xử lý các lỗi setup thường gặp

---

## Mục Tiêu Bài Học

Sau bài này bạn chạy được cùng app của khóa học trên iOS Simulator, từ Xcode và từ terminal, và hiểu Kotlin code đi vào app iOS bằng đường nào.

Đây là bài tùy chọn. Nó cần một máy Mac. Nếu bạn dùng Windows hoặc Linux, bỏ qua bài này: mọi phần còn lại của khóa đều làm được trên Android.

## Bạn Cần Gì

| Yêu cầu                    | Ghi chú                                                                                                                       |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Mac chip Apple Silicon     | Các KMP module chỉ khai báo target `iosArm64()` và `iosSimulatorArm64()`, nên Simulator trên Mac chip Intel không chạy được  |
| Xcode                      | Tải từ [Mac App Store](https://apps.apple.com/app/xcode/id497799835). Mở một lần để Xcode cài thêm components và iOS Simulator |
| JDK 17                     | Cùng JDK 17 dùng cho Android. Xcode gọi Gradle để build phần Kotlin                                                            |
| Bản Android đã chạy        | Android build và test pass trước, để lỗi iOS không lẫn với lỗi setup chung                                                    |

Chạy trên Simulator không cần Apple Developer Program. Chạy trên iPhone thật thì cần một Apple ID và một team trong Xcode, phần cuối bài có nói.

## Phần 1: Kotlin Đi Vào App iOS Như Thế Nào

Trên Android, `:app` là một Gradle module và depend thẳng vào code Kotlin. iOS không đọc được Gradle module. Xcode chỉ link với framework.

Vì vậy module `:shared-app` build toàn bộ code Kotlin dùng chung (các feature, `:core`, `:data`, `:domain`...) thành một static framework tên `Shared`. Project Xcode trong `iosApp/` có một build phase tên "Build Shared framework" chạy lệnh:

```bash
./gradlew :shared-app:embedAndSignAppleFrameworkForXcode
```

Phía Swift chỉ có hai file nhỏ:

- `iosApp/iosApp/iOSApp.swift` gọi `SharedKoinKt.doInitApp()` khi app khởi động.
- `iosApp/iosApp/ContentView.swift` bọc `MainViewControllerKt.MainViewController()`, một `UIViewController` chứa toàn bộ giao diện Compose Multiplatform.

Mọi màn hình bạn thấy trên iOS đều là code Kotlin trong `commonMain`, giống hệt trên Android.

Mô hình này (một framework Kotlin, một app shell Swift mỏng, Xcode gọi Gradle trong build phase) là cách tích hợp phổ biến của các project KMP. Tham khảo: [iOS integration methods](https://kotlinlang.org/docs/multiplatform/multiplatform-ios-integration-overview.html).

## Phần 2: Tạo `Config.xcconfig`

Android đọc Supabase URL, các key và flag từ `local.properties`. iOS không có `BuildConfig`, nên giá trị đi đường khác:

```text
iosApp/Configuration/Config.xcconfig
-> Info.plist (các dòng $(SUPABASE_URL), $(SUPABASE_PUBLISHABLE_KEY)...)
-> iosAppBuildConfig() trong shared-app/src/iosMain/.../SharedKoin.kt
-> AppBuildConfig, cùng data class mà Android điền từ BuildConfig
```

Tạo file từ template ở repo root:

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

`Config.xcconfig` đã nằm trong `.gitignore`. Mở nó và điền:

| Key                             | Giá trị                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| `SUPABASE_URL`                  | Project URL của Supabase, viết theo dạng `https:$(SLASH)$(SLASH)your-project.supabase.co` |
| `SUPABASE_PUBLISHABLE_KEY`      | Cùng publishable key (`sb_publishable_...`) bạn dùng ở `local.properties`            |
| `SUPABASE_ANON_KEY`             | Để trống. Đây là fallback tạm thời, chỉ được đọc khi `SUPABASE_PUBLISHABLE_KEY` trống |
| `GOOGLE_OAUTH_CLIENT_ID`        | Web client ID, cùng giá trị `google.server.clientId` của Android                     |
| `GOOGLE_IOS_CLIENT_ID`          | iOS OAuth client riêng. Để trống được nếu chưa làm Google Sign-In                    |
| `GOOGLE_IOS_CLIENT_ID_REVERSED` | Chính iOS client ID viết ngược, dạng `com.googleusercontent.apps.<id>`               |
| `PRODUCT_BUNDLE_IDENTIFIER`     | Giữ `com.creative.kta`                                                               |
| `DEVELOPMENT_TEAM`              | Để trống khi chạy Simulator                                                          |
| `REVENUECAT_API_KEY`            | Key `appl_` của RevenueCat. Không dùng key `goog_` của Android                        |
| `PREMIUM_GATE_ENABLED`          | Giữ `false`                                                                          |

Vì sao có dòng `SLASH = /`? Trong file xcconfig, `//` mở đầu một comment. Nếu bạn dán thẳng URL đầy đủ, giá trị bị cắt ngay sau `https:`. Template định nghĩa `SLASH` riêng rồi ghép lại.

Flag `PREMIUM_GATE_ENABLED` chỉ bật khi giá trị đúng bằng `true`. Gõ sai thì tính năng trả phí bị tắt, và đó là hướng an toàn.

### Google Sign-In Trên iOS

Google không cho một web client redirect về custom URL scheme của app, nên iOS cần một iOS OAuth client riêng trong Google Cloud Console, tạo với bundle ID `com.creative.kta`. Sau khi tạo:

1. Điền client ID vào `GOOGLE_IOS_CLIENT_ID` và bản viết ngược vào `GOOGLE_IOS_CLIENT_ID_REVERSED`. `Info.plist` khai báo bản viết ngược trong `CFBundleURLSchemes` để Google redirect về được app.
2. Thêm iOS client ID này vào Google provider trong Supabase Auth, nếu không Supabase từ chối token vì sai audience.

Lúc khởi động, hàm `checkGoogleSignInConfiguration` log cảnh báo nếu `GOOGLE_IOS_CLIENT_ID` trống, và log lỗi nếu URL scheme trong `Info.plist` không khớp với client ID.

## Phần 3: Chạy Từ Xcode

1. Mở `iosApp/iosApp.xcodeproj` bằng Xcode.
2. Chọn scheme `iosApp` và một iPhone Simulator ở thanh công cụ.
3. Bấm Run (`Cmd + R`).

Lần build đầu rất chậm. Gradle tải toolchain Kotlin/Native rồi compile mọi module cho iOS. Các lần sau nhanh hơn nhiều nhờ cache. Theo dõi tiến độ trong Report navigator của Xcode, mục build phase "Build Shared framework".

App mở lên với cùng màn hình Splash và Menu như Android, kể cả mọi chuỗi bạn đã sửa trong `commonMain`.

## Phần 4: Build Từ Terminal

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

Thay `iPhone 16` bằng tên một Simulator có trên máy bạn (`xcrun simctl list devices available` liệt kê chúng).

Đừng dùng `-destination 'generic/platform=iOS Simulator'`. Destination generic yêu cầu build cả kiến trúc x86_64, trong khi không module nào khai báo target đó, và bước sync Compose resources sẽ fail.

## Phần 5: Chạy Test Trên iOS

Trên Mac, `allTests` của mỗi KMP module chạy test trên cả JVM và iOS Simulator:

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

`commonTest` chạy hai lần, một lần trên JVM và một lần trên Kotlin/Native. Một test có thể pass trên JVM và fail trên iOS, ví dụ khi code chung vô tình dựa vào hành vi chỉ JVM mới có. Vì vậy Android xanh chưa có nghĩa là iOS xanh.

## Khác Biệt Bạn Sẽ Thấy Trên iOS

| Tính năng        | Android                              | iOS                                                                |
| ---------------- | ------------------------------------ | ------------------------------------------------------------------ |
| AI hint          | Gemini nếu Remote Config có key, không thì gợi ý local | Luôn dùng gợi ý local. iOS chưa nối Remote Config            |
| Analytics        | Firebase Analytics                   | Implementation rỗng (`NoOpAnalyticsManager`)                       |
| Daily challenge  | Theo flag `daily.challenge.enabled`  | Luôn tắt                                                           |

Các khác biệt này nằm ở Koin binding của từng nền tảng (`DataModule.ios.kt` so với `DataModule.android.kt`). Giao diện chung không có nhánh `if (platform)` nào.

## Bảng Xử Lý Lỗi

| Dạng lỗi                                                         | Kiểm tra trước                                                                                                                         |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Build phase "Build Shared framework" báo không tìm thấy Java (ví dụ `Unable to locate a Java Runtime`) | Build phase của Xcode không đọc `~/.zshrc`, nên `JAVA_HOME` đặt ở đó không có tác dụng. Cài JDK 17 bằng file `.pkg` hoặc `brew install --cask temurin@17` để JDK nằm trong `/Library/Java/JavaVirtualMachines` |
| `Cannot find a Java installation ... languageVersion=17`         | Máy chưa có JDK 17. Cài JDK 17                                                                                                        |
| `Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56`   | `KmpLibraryConventionPlugin.linkSwiftRuntimeShims` dùng `xcode-select -p` để tìm Swift libraries. Chạy `xcode-select -p` và xác nhận nó trỏ vào Xcode app, không phải Command Line Tools |
| Xcode báo không mở được file `Config.xcconfig`                   | Chưa tạo `iosApp/Configuration/Config.xcconfig` (Phần 2)                                                                               |
| Đăng nhập email chạy trên Android nhưng fail trên iOS            | Kiểm tra `SUPABASE_URL` có bị cắt ở `//` không. Dùng `$(SLASH)$(SLASH)`                                                                 |
| Lỗi khi dùng `generic/platform=iOS Simulator`                    | Dùng destination có tên Simulator cụ thể (Phần 4)                                                                                      |
| Google Sign-In mở browser rồi không quay về app                  | `GOOGLE_IOS_CLIENT_ID_REVERSED` không khớp `GOOGLE_IOS_CLIENT_ID`. Đọc log lỗi của `checkGoogleSignInConfiguration`                     |
| Lỗi signing khi chạy trên iPhone thật                            | Điền `DEVELOPMENT_TEAM` bằng team ID mà Apple ID trên máy bạn thuộc về                                                                 |

## Checkpoint

Bạn hoàn tất bài này khi:

- App chạy trên iOS Simulator và vào được Menu,
- Splash và Menu hiện giống bản Android,
- Lệnh `xcodebuild` ở Phần 4 kết thúc với `BUILD SUCCEEDED`,
- `git status` không liệt kê `iosApp/Configuration/Config.xcconfig`.

---

- Bài trước: [Build, Test và Commit Đầu Tiên](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-05-first-build-test-commit.md)
- Bài tiếp theo: [Đọc Module Graph](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-01-read-module-graph.md)
