Dan Tech Academy
Sau bài này bạn sẽ
  • 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
Thời lượng20 phút
Độ khóBeginner

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 đang cập nhật

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.

Bản quay đang trong khâu hậu kỳ

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ầuGhi chú
Mac chip Apple SiliconCác KMP module chỉ khai báo target iosArm64() và iosSimulatorArm64(), nên Simulator trên Mac chip Intel không chạy được
XcodeTải từ Mac App Store. Mở một lần để Xcode cài thêm components và iOS Simulator
JDK 17Cùng JDK 17 dùng cho Android. Xcode gọi Gradle để build phần Kotlin
Bản Android đã chạyAndroid 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.

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:

KeyGiá trị
SUPABASE_URLProject URL của Supabase, viết theo dạng https:$(SLASH)$(SLASH)your-project.supabase.co
SUPABASE_PUBLISHABLE_KEYCù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_IDWeb client ID, cùng giá trị google.server.clientId của Android
GOOGLE_IOS_CLIENT_IDiOS OAuth client riêng. Để trống được nếu chưa làm Google Sign-In
GOOGLE_IOS_CLIENT_ID_REVERSEDChính iOS client ID viết ngược, dạng com.googleusercontent.apps.<id>
PRODUCT_BUNDLE_IDENTIFIERGiữ com.creative.kta
DEVELOPMENT_TEAMĐể trống khi chạy Simulator
REVENUECAT_API_KEYKey appl_ của RevenueCat. Không dùng key goog_ của Android
PREMIUM_GATE_ENABLEDGiữ 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ăngAndroidiOS
AI hintGemini nếu Remote Config có key, không thì gợi ý localLuôn dùng gợi ý local. iOS chưa nối Remote Config
AnalyticsFirebase AnalyticsImplementation rỗng (NoOpAnalyticsManager)
Daily challengeTheo flag daily.challenge.enabledLuô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ỗiKiể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=17Máy chưa có JDK 17. Cài JDK 17
Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56KmpLibraryConventionPlugin.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.xcconfigChưa tạo iosApp/Configuration/Config.xcconfig (Phần 2)
Đăng nhập email chạy trên Android nhưng fail trên iOSKiểm tra SUPABASE_URL có bị cắt ở // không. Dùng $(SLASH)$(SLASH)
Lỗi khi dùng generic/platform=iOS SimulatorDùng destination có tên Simulator cụ thể (Phần 4)
Google Sign-In mở browser rồi không quay về appGOOGLE_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.
Đăng nhập