DEV Community

Cover image for [KMP] I Had Never Written an iOS App. The iOS Version Took Two Days
Kanon Fujita
Kanon Fujita

Posted on Edited on

[KMP] I Had Never Written an iOS App. The iOS Version Took Two Days

I'm building an app called Afterfade on my own — seven five-second videos recorded over a day become one lo-fi track. The iOS version is live on the App Store; the Android version is in review.

I had some Android experience but had never built an iOS app. With only evenings and weekends, implementation came to six days for Android and two for iOS. Eight days total.

It is built with Kotlin Multiplatform (KMP) and Compose Multiplatform — iOS support went stable in May 2025. This post covers why I picked KMP, and what did and did not end up shared.

I counted the days to the store before writing anything

This was built for RevenueCat Shipaton, a two-month hackathon that requires you to be live on a store. Google Play demands 14 consecutive days of 12 testers in a closed test before you can even request production access — weeks pass while you do nothing. So: finish Android first, start the closed test, and spend those unmovable 14 days building iOS.

The options for shipping to two platforms alone

Writing each one natively doubles the work. Flutter or React Native let you write once, but the language becomes Dart or TypeScript. KMP + Compose Multiplatform keeps the language as Kotlin. I already knew Kotlin, so this is what I picked.

What paid off later was that for the first week I wrote a plain Android app without thinking about iOS at all, then added the iOS target. Pick Flutter or React Native and that week starts with rewriting in Dart or TypeScript. If you already have an Android app, KMP is the only option where the second platform is something you add rather than something you restart.

You can drop down to native later, and only where you need it

KMP gives you expect/actual: shared code declares a capability, and each platform writes the implementation.

// commonMain (shared by both platforms) — declaration only
@Composable
expect fun InterstitialAdEffect(
    show: Boolean,
    placement: String,
    onShown: () -> Unit = {},
    onComplete: () -> Unit,
)
Enter fullscreen mode Exit fullscreen mode

The implementation calls that platform's APIs directly — the iOS camera is 320 lines of Kotlin calling AVFoundation as-is, the same API names you would write in Swift. This is also the escape hatch when an SDK has no KMP support; RevenueCat and AdMob have no shared KMP SDK, so each platform uses its official native SDK directly.

What gets shared and what does not

App structure: commonMain (green), shared by both platforms, holds the music engine, 2,236 lines with no external dependencies, and the screens and app logic, 6,775 lines of Compose Multiplatform. Platform-specific code (red) holds audio extraction from video, camera, playback and export, and the billing and ad SDKs. Only one arrow crosses between them, from the engine to audio extraction.

Only one line runs from the engine into red: pulling audio data out of a video file. Everything past that — analysis, arrangement, mixing, rendering — is shared. This app draws every component by hand, so all that polish lands on both platforms at once.

Moving the music engine onto the device

Music generation originally ran in Python on AWS Lambda — slow, costly, offline-unusable, and users' videos had to leave the device. The engine is pure computation with no external dependencies, so writing it in Kotlin means it runs on both platforms as-is. I ported it on day 5 and deleted the server the next day.

One review comment fixed both platforms and four languages

App Store review flagged my permission flow: once you show a pre-permission explanation, it has to lead straight into the system dialog, with no path to defer. The Android version had the same loose flow, and that screen lives in shared code. I fixed both at once. The copy lives in one shared resource set, so four languages were a single change too.

What did not get shared

Only two things ate real time, and both happened where I tried to write platform-specific work in shared code.

Video export got rewritten in Swift

iOS's asynchronous APIs and Kotlin made video export hang forever. I wrapped AVAssetExportSession's callback in suspendCancellableCoroutine; the callback never fired and the coroutine waited forever. I spent a day on it, then rewrote the AV work in Swift. The iOS "two days" includes that day.

Swift is 1,199 lines, 9.4% of the total — all video work and billing/ad SDK calls. I wrote that up in another post.

Some of the iOS feel comes for free, some does not

Haptics and the scrolling photo grid worked on both from shared code. What did not come for free was the swipe-back gesture (52 lines) and making the iPad share sheet appear as a popover. This app draws every component itself, though. Standard UI components would have more gaps to fill.

2,216 lines were all the second platform needed

commonMain  9,011   androidMain 1,537   iosMain 1,017   Swift 1,199
Enter fullscreen mode Exit fullscreen mode

Everything written for iOS is the two columns on the right: 2,216 lines. 70.6% shared, and the ~3,700 platform-specific lines are almost entirely camera, video, billing and ads. Estimated two weeks for Android took six days; estimated ten days for iOS took two.

I got a first iOS app onto the App Store because only 2,216 lines were left. Shipping to a second platform is a question about the store, not about how much there is to build.


Afterfade is being built for RevenueCat Shipaton 2026.

Top comments (0)