For seven posts, every claim about iOS in this chapter ended the same way: compiled, not linked or run. The machine the chapter was written on has no Xcode, and Kotlin/Native can compile iOS code without it but cannot link a framework or run a test. The build post wrote a CI workflow that would do both on a machine that has Xcode, and left it unrun.
This post runs it. The final version of the reading list now lives in a public repository of its own, fauzisho/reading-list-kmp, and GitHub Actions builds and tests it on Linux, on an Android emulator and on macOS, on every push. Here is what the runs measured.
1. Three Jobs, Green on the First Push
The workflow is the one from the build post, extended for the sync module and the server of the last post:
| Job | Runner | What it does |
|---|---|---|
check | ubuntu-latest | compiles the shared metadata and every target but iOS; runs the tests on the JVM and in Chrome; builds the release APK, the web bundle and the server |
android | ubuntu-latest, KVM | starts an Android 36 emulator and runs the shared tests on it |
ios | macos-latest | links the iOS framework and runs the shared tests on the iOS simulator |
The first push went green in all three jobs, with no change to the code. That is a result in itself, and it is not an accident: the iOS code had been compiled after every change for seven posts, and the type checker had caught everything a compiler can catch. What compiling could not show was that the iOS framework links, and that the code behaves the same on iOS as elsewhere. Both are now shown:
| Where | Tests | Failed | What ran |
|---|---|---|---|
| JVM | 29 | 0 | merge, repository, navigation, timing, migration, the seven sync stories against the real server, the desktop UI tests |
| Chrome (Kotlin/Wasm) | 19 | 0 | merge, repository, navigation, timing |
| Android 36 emulator | 13 | 0 | repository, navigation, timing (the shared tests, as instrumented tests) |
| iOS simulator | 19 | 0 | merge, repository, navigation, timing: the database tests on iOS for the first time, with the bundled SQLite |
The counts differ only by what each platform can run: the sync stories and the migration test start a JVM server and use JVM-only helpers, and the Android device tests compile commonTest of the app module, not of :sync. Every test that can run on a platform ran there and passed. The counts come from a small step at the end of each job that reads the test reports and publishes them as annotations on the run’s page, so they can be checked without the logs.
The database on iOS, measured
The shared timing test from the Room post prints which SQLite it ran and how fast; the summary step publishes those lines too. On the CI runners:
| Where (CI runner) | SQLite | 200 inserts, one transaction each | 1,000 in one transaction | 50 reads of 1,200 rows |
|---|---|---|---|---|
JVM (ubuntu-latest) | 3.50.1, bundled | 66 ms | 3.2 ms | 120 ms |
Chrome, Wasm (ubuntu-latest) | 3.53.4, in a web worker | 548 ms | 119 ms | 815 ms |
iOS simulator (macos-latest) | 3.50.1, bundled | 36 ms | 7.4 ms | 522 ms |
iOS runs the same bundled SQLite as the desktop, and writes are fast. Reading 1,200 rows back into Kotlin objects is about four times slower than on the JVM; where that time goes (Kotlin/Native’s allocation and garbage collection, Room’s generated code, or the simulator) was not investigated, so the numbers are reported, not explained. The Android emulator’s timing line does not appear: Android’s instrumented tests write their output to the device log, not to the test report. The runners are different machines from the ones in the earlier posts, so these numbers compare platforms within one table, not with the earlier tables.
2. What a Run Costs
| Job | First run (empty caches) | Second run (Gradle’s caches restored) |
|---|---|---|
check | 9:36 | 7:11 |
android | 5:59 | 3:58 |
ios | 10:56 | 8:38 |
| the whole run | 20:46 | 16:02 |
The second run differed only by a two-line change to the workflow, so gradle/actions/setup-gradle could restore the dependencies and the build cache of the first. Compiling every target in the check job went from 2:49 to 0:52. The jobs that run after check start in parallel, so a run takes about as long as check plus the slower of the other two: the iOS job, whose link and simulator tests take most of its time.
For a public repository, GitHub’s hosted runners are free. For a private one, the published prices are 0.062 per minute on macOS, about ten times as much; at the second run’s durations, one push would cost about $0.60, most of it the iOS job. A private project that pushes often would run the iOS job less often: nightly, or on the main branch only.
The runs also surfaced two notices that no local build would: that ubuntu-latest was moving to a new Ubuntu release, and that macOS arm64 runners may queue under load. Neither failed anything, and both are the kind of thing a CI pipeline exists to say early.
3. What Changes in the Chapter
Seven posts said the iOS code was compiled and not run. That is now out of date in the useful direction:
| Earlier claim | Now |
|---|---|
| ”iOS code compiles; linking needs Xcode.” (posts 1–7) | Linked in CI on macos-latest, on every push. |
| ”The shared tests ran on the JVM, in Chrome and on Android.” (posts 3–5) | And on the iOS simulator: 19 tests, 0 failures. |
| ”The CI workflow was written and validated, not run.” (post 6) | Run: three jobs green, 16:02 per push with caches. |
The repository is the reading list as it stands after the sync post, with the workflow, the test-count step and a README; the build post explains the workflow line by line. Each post’s own download remains the version that post describes.
Common Claims, Checked
| Claim | Verdict |
|---|---|
| ”You need a Mac to develop iOS with KMP.” | You need one to link and run iOS code. This chapter wrote all of it without one and used a hosted Mac in CI for the rest; the first run on it passed. |
| ”Code that compiles for iOS may still fail there.” | It may; here it did not: 19 tests passed on the simulator on the first run, including the database tests with the bundled SQLite. |
| ”Multiplatform CI is slow.” | 16:02 per push with caches, three jobs in parallel; compiling every target took 0:52. |
| ”macOS CI is expensive.” | Free for public repositories; on private ones about ten times the Linux rate, here about $0.60 per push. |
References
- The repository: fauzisho/reading-list-kmp, and its workflow runs.
- GitHub. About billing for GitHub Actions: free for public repositories; per-minute rates for private ones.
- GitHub. gradle/actions setup-gradle and ReactiveCircus/android-emulator-runner.
- JetBrains. Test your multiplatform app, including on the iOS simulator, and compiling Apple targets on other hosts.