If you’ve been living under a rock for a year, I have shocking news about the election*, and great news about the future of Swift portability.
Swift for Android is here.
More specifically, a quite early-days developer preview is here.
This tool is in its infancy, but formalises over a decade of homegrown toolchain hacks used by cross-platform wonks to enable Android to talk to Swift code.
As it happens, I’ve got a touch of experience using Kotlin Multiplatform, a.k.a. KMP. It’s always been the most palatable cross-platform solution for native mobile devs, since it works by sharing business logic in a cross-platform module, but allowing for fully native iOS and Android UI code.
I figured, let’s get ahead of the debate/war your iOS and Android teams are inevitably going to have and compare the developer experience of both cross-platform tools.
Now that I have nothing better to do with my time, I spent the week playing with Swift for Android, and KMP, setting up a pair of cross-platform apps based on Rollercoaster Tycoon 2. Swift for Android powers the Android app (with Jetpack Compose), and KMP powers the iOS app (with SwiftUI).
Here’s what I learned.
*I didn’t say which election, please project your biases favourably
I open-sourced both projects. If you want to start your own Swift for Android project (or KMP for that matter), you can fork it to use my project as a baseline:
https://github.com/jacobsapps/SwiftAndroid-vs-KMP
Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on concurrency, SwiftUI, iOS performance, and Swift internals in your inbox every week.
tl;dr
I’m not going to be a jerk about it, because Swift for Android is brand-new and not released for production. The big announcement was to declare the existence of nightly builds for the dev preview.
It’s been built by Swift and Android toolchain gods who have spent a long time doing it the Hard Way™, and I can only assume the new tooling is an enormous improvement on what was possible before.
But… it might need a couple more months in the oven before I can recommend refactoring your production app. Things are moving fast, though: I had to rebuild my sample app the morning I published this because they added async-await compatibility.
In a sane world, this article would be 80% me whinging about the setup experience, but I’m a professional, so I’ll try to cap it to 25% at most.
The bit with lots of code to keep you awake
I’m going to do something weird and put a nice code comparison at the beginning, so all the iOS developers can look at the Kotlin and think “boy, am I glad I don’t have to write that”, and everyone who’s an Android developer can look at the Swift and think “ew, who would want to write that on Android”.
My sample apps have 5 cross-cutting components.
Server
A simple ExpressJS server that serves a JSON API over localhost
Android+Swift/Core
A Swift package that fetches data from the network and decodes it into data models. Network requests returning models are exposed via a public service.
Android+Swift/Coasters
Android app that calls into the Core Swift module, queries the service for rollercoaster data, then displays it on a list.
iOS+KMP/shared
The KMP twin brother of Android+Swift/Core. Also fetches data from the network and serialises it into data models, exposing a public service.
iOS+KMP/iOSApp
iOS app which calls into the shared KMP module, queries the service for rollercoaster data, then displays it on a list.
Let’s look at how the code differs between the two languages.
Cross-platform module code
Let’s start by looking at some Swift code we’ll use in Android, then some Kotlin code we’ll use on iOS. We’ll dip our toes into data model setup, then look at how to implement networking via cross-platform service injections.
Data Models
Swift data models use Codable to automatically parse out JSON data into a struct, a value type useful for simple data transfer.
Android apps call into Swift code via JNI bindings generated from our code using swift-java. JNI, or Java Native Interface, allows Java code running on the JVM to talk to other languages. swift-java is the interoperability layer that creates Java interfaces from our Swift module code (we’ll look at these soon), allowing it to be compiled as a native Android library.
Marshalling arrays and handling Foundation types like URLs across the module boundary can be brittle and error-prone, so I created some simple bridging code to expose primitive-typed getters for Foundation types like URL and implementing array indexing.
Please avoid looking through my commit history to see how I landed here: my first iteration literally yeeted the JSON string across the module boundary. I expect before Swift for Android becomes Production Ready™, there will be a simpler, idiomatic way to pass things like arrays across the module boundary.
Kotlin stores data in a data class, a reference type for which Kotlin synthesises a bunch of value-like behaviour for copying, equality, and printing.
It uses a @Serializable data class to store data, which basically works just like Codable. If you’re used to the giant umbrella module that is Foundation, it might look strange to individually import every package like kotlinx.serialization.Serializable, but this is how pretty much every ecosystem outside iOS does it (the IDE automates it).
Now that we’re sufficiently warmed up, the networking logic begins to get interesting.
Networking
The Swift code looks like run-of-the-mill networking code, except for the imports.
This conditional import block is required on non-Darwin platforms.
#if canImport(FoundationNetworking)
import FoundationNetworking
#endifThis imports the open-source, cross-platform, Swift re-implementation of Foundation’s network libraries to allow the basic Foundation APIs like URLSession to work on other platforms*.
*This is explained really well in Pierluigi Cifani’s excellent talk, Android Doesn’t Deserve Swift—But We Did It Anyway.
If you want to go deeper on swift-foundation, you can also check out my recent work:
The KMP project uses idiomatic Kotlin libraries like Ktor’s HttpClient to perform network requests asynchronously using a suspend fun.
Kotlin Multiplatform has a very neat way of neatly injecting platform-specific code. First, you declare a provider file with an expect fun.
Now the actual fun begins. For each platform you’re implementing on, you set up the expect fun’s implementation: “actual fun”. Here, we can inject Ktor’s built-in HTTP client for Darwin platforms.
It’s also possible to eschew the expect/actual pattern, and use a pure DI approach: Kotlin interface with implementation object injected into the KMP side from native code.
I’m not sure there’s a way to achieve the same using Swift for Android. I expect platform-specific implementations, plus the accompanying ergonomics, will come as the platform evolves. You can already do a lot using the vanilla swift-foundation frameworks, and 25% of the Swift Package Index has already been cross-compiled to work on Android, so I reckon we should be okay for now.
Calling into Multiplatform Code
I’ve shown you how you can write data models, networking, and simple business logic in your preferred language. It’s an amazing leap that we can now call into multiplatform code from an app using the platform’s native UI language.
But this boundary between native and cross-platform code is where everything can fall over.
If the cross-platform library has rough edges, devs are gonna get cut. 🩸
Calling Swift from Android
My simple rollercoaster project on Android uses a cookie-cutter Repository pattern, along with Coroutines, to call into our Swift library code.
Once we’ve successfully compiled our Swift package as an Android library (using some arcane Gradle incantations, more on this later), we can import the package like any other Android module.
import com.jacob.core.RollerCoasterServiceThe networking service is instantiated using SwiftArena, a part of swift-java. SwiftArena basically manages the lifetime of Swift objects on the JVM. Think of it kind of like an @autoreleasepool.
Our fetchAll() method calls straight into the service’s interface, which fetches and returns our data models. The Swift async/await interface is converted into a CompletableFuture in the JNI bindings. We call the getter methods on our model object to convert it into native Kotlin objects for use in our UI layer.
If we CMD+Click into the model, we can see the Java interface that’s created when swift-java builds the Swift code as an Android module:
Here we can see the java.util.concurrent.CompletableFuture created from our async Swift interface that allows the asynchronous operations to interoperate.
Let me clarify something though (or, more specifically, Pierluigi Cifani clarified it on his proofread. Thanks!)
The Swift code we call into is compiled Swift code, running on the Swift runtime. The interface here is just the binding that allows it to be called from Android’s JVM. Objects in our Swift code, like our service, can be referenced from inside the JVM, and SwiftArena helps to manage memory at this boundary.
Calling Kotlin from iOS
Idiomatically, the KMP shared library is called “Common”. Or, sometimes, “Shared”. You can modularise the library as much as you like, but you will need to expose this to the platform apps as a single umbrella framework. This is pretty annoying for namespacing, but you stop noticing it pretty fast*.
*Alex Bush told me, actually, there’s a beta of Swift Export for KMP that resolved the umbrella framework issue, allowing multiple modules to be used from KMP in iOS.
We perform JSON mapping here to give our SwiftUI view a nice value type to work with, but it’s not mandatory in this case. Using DTOs is a very common practice when working across module boundaries, even outside of cross-platform apps.
Let’s CMD+Click into one of the Shared functions:
Here we see the inner guts of how KMP works: it generates Objective-C headers for all your Kotlin code.
This is actually why KMP has existed since 2017, whereas we only just got around to Swift for Android in late 2025. When Kotlin compiles into machine code, its compiler is able to generate Objective-C headers for this code, and Swift was built from day 1 to be compatible with Objective-C.
The Swift for Android team had a hell of a job cut out for them going the other way around: there was no JVM-compatible toolchain just lying around, and no convenient compatibility layer, so they had to hack it together from scratch.
When you’re running a long-term KMP project, you might want to use SKIE (Swift Kotlin Interface Enhancer). There’s a few rough edges around things like suspend functions, interfaces, and sealed classes which the library sands down into idiomatic Swift signatures. Other projects might want to manually implement wrappers around the KMP interface, converting Coroutines to things like Combine publishers, RxSwift observables, or AsyncStreams.


















