- 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
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.
Video bài học sắp lên sóng
Trong lúc chờ, bạn hãy đọc bản text của bài học bên dưới: đầy đủ lý thuyết, code mẫu và checklist thực hành.
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. 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:
./gradlew :shared-app:embedAndSignAppleFrameworkForXcode
Phía Swift chỉ có hai file nhỏ:
iosApp/iosApp/iOSApp.swiftgọiSharedKoinKt.doInitApp()khi app khởi động.iosApp/iosApp/ContentView.swiftbọcMainViewControllerKt.MainViewController(), mộtUIViewControllerchứ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.
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:
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:
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:
- Điền client ID vào
GOOGLE_IOS_CLIENT_IDvà bản viết ngược vàoGOOGLE_IOS_CLIENT_ID_REVERSED.Info.plistkhai báo bản viết ngược trongCFBundleURLSchemesđể Google redirect về được app. - 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
- Mở
iosApp/iosApp.xcodeprojbằng Xcode. - Chọn scheme
iosAppvà một iPhone Simulator ở thanh công cụ. - 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
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:
./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ớiBUILD SUCCEEDED, git statuskhông liệt kêiosApp/Configuration/Config.xcconfig.