Kombine in June: an iOS app fell out of the migration#

The headline for June is short: Kombine has an iOS app now. Not a wrapper, not a demo — a real iOS (and watchOS) app with sync, timers, notifications, and Live Activities. The longer story is how it got there, because nobody sat down and “built an iOS app.” It dropped out the back of a Kotlin Multiplatform migration that had been grinding for weeks.

My original plan was to develop Webapp first, and its kinda working, but I don’t have passion for webdev anymore, and I was curious how much work, the shared KMP and Compose code, would be to have something working on iOS.

And I overdelivered, app is now in TestFlight in App Store, already approved for release :D

How a refactor became a new platform#

Going into June the app was Android-only. The plan wasn’t “ship iOS” — it was “stop keeping two of everything.” So the migration went layer by layer:

  • Network and serialization moved to a shared Ktor + kotlinx.serialization module. Retrofit and Moshi got deleted from the Android side entirely.
  • Local storage went from Room to SqlDelight, running in commonMain — the same database code on both platforms.
  • DI moved from Hilt to Koin, since Hilt is Android-only.
  • The timer state machine, business logic, ViewModels, design system, and navigation all moved into shared code.

Then the part that always feels good: deleting the scaffolding. 70 typealias shims that mapped old Android types to new KMP types — gone. Whole directories of Android-only event handler duplicates — gone. By the end, settings, stats, and sync state all lived in shared code, and CI grew architecture-boundary tests that fail the build if someone reintroduces an app-vs-shared duplicate. The migration can’t quietly rot back.

And once the domain, design system, and data layer were all in commonMain, the iOS app was mostly a question of wiring up the platform shell. So that’s what happened next.

The iOS app, week by week#

  • It started as a design-system showcase running on the iOS simulator via Compose Multiplatform — proof the components render correctly off Android.
  • Then the full shell came alive: task picker, timer settings, sidebar, themes, auth token persistence, recurring-task completion, all the screen animations.
  • Live Activity while a timer runs — a live countdown with pause/stop you can hit without opening the app.
  • Push notifications via FCM → APNs, with the backend fixed to target iOS devices with the right payload.
  • Daily digest + per-task reminders running off iOS background tasks, catching up to what Android already did.
  • TestFlight CI — after several rounds of fighting Xcode 16’s export process.

A watchOS companion went from nothing to fully functional in a single week: timer face, pickers, complications in all four families, Smart Stack, offline sync with a connection indicator. The Wear OS and watchOS complications both render live timer progress and open the app on tap, and the watch starts a timer instantly instead of waiting on a phone round-trip.

The product kept moving anyway#

The migration wasn’t the only thing shipping.

  • Goals & Daily Toad on web — a planner with four horizon cards (Annual → Weekly), inline editing, goal history. Daily Toad floats your most-important task to the top of the timer picker.
  • Activity tagging — link tasks to an activity, and Stats breaks down focus time by activity and by task on phone and web.
  • Recurring tasks, overhauled — deleting one now cascades to future occurrences instead of orphaning them; a new “after completion” mode anchors the next occurrence on when you actually finished (chores, habits) rather than a fixed deadline; and completing one advances the task in place instead of spawning a duplicate row per occurrence — which had been quietly inflating weekly goal counts.
  • Show/hide completed tasks + sort options, on mobile and web.
  • In-app feedback (star rating + notes) posting straight to the backend.
  • Dashboard redesign on web — a live timer strip while a session runs, a richer KPI row, and a today-as-timeline activity chart.

And a pile of sharp edges, filed down#

A lot of June was unglamorous correctness work, the kind that doesn’t demo but is the difference between “works on my device” and “works”:

  • “Goal” meant three different things — long-term goals, daily session targets, and daily task picks — all under one overloaded name. Untangled into three distinct concepts, renamed consistently across backend, API, and clients. No behavior change, no data migration, just a name that finally tells the truth.
  • Daily-goal sync bugs, a whole cluster: wrong case sent to the server, a missing notification scope, server changes not applying back to the device, a hardcoded webapp page silently dropping the weekly goal card. Goals and daily picks now sync consistently across phone, watch, and web.
  • Sync hardening — idempotency keys so a retried sync can’t double-apply, deletes as proper tombstones instead of hard deletes, and a fix for sync workers silently dropping off at startup.
  • A deadline time-unit bug that read deadlines in the wrong unit and pushed “due today” math thousands of years off — fixed on both platforms.
  • Session recovery after the app is killed now accounts for clock changes, so a restored timer can’t end up with a corrupted duration. Session summaries (rating, notes) are saved instead of silently discarded.
  • Login/session handling hardened repeatedly — no more infinite splash hang on a dead session, no more “logged in with a dead token” state that blocked both logout and re-login, no more duplicate “session expired” spam.
  • Backend resilience — background goroutines (WebSocket broadcasts, push, reminders) now recover from panics instead of taking the whole process down.

Where that leaves things#

The bet of the month — that a KMP migration would land iOS almost as a side effect — paid off. Kombine went from one platform to four (Android, Wear OS, iOS, watchOS) on a shared core, with CI guardrails to keep it shared. The migration is fully wrapped. Now the interesting question is the nicer one to have: with the platform multiplied, what’s worth building next? I need to finish webapp I guess…