Swift 6.3 on Android, Three Weeks In: What Actually Broke and What Surprised Me

Mario 9 min read
Two phones side by side in different operating systems — visual stand-in for Swift code now compiling natively for both iOS and Android.

Three weeks ago I wrote about Apple’s Swift 6.3 Android announcement on a Tuesday morning, with the SDK installed for about ninety minutes and a “Hello, World” running on an emulator. I called the post “first-person test of the new Apple SDK” and meant it — first-person, first impression, the entire honest scope of what I had time for that morning.

Then I did the thing you’re supposed to do with new technology and didn’t write about it for three weeks. I shipped something with it. This is what I learned.


What I tried to ship

The smallest possible bet: take the part of ThinkBud’s sync engine that handles “review one card → write the result to the spaced-repetition database,” and run it on Android.

Not the UI. Not the AI card generator. Not auth. Not network. Just one boring, well-tested piece of pure Swift business logic — ReviewOutcome calculations, the SuperMemo-2 algorithm, the database write — and stand up a tiny Android UI in Kotlin that calls into it.

The point of picking this slice was that I knew it cold on iOS. If something behaved differently on Android, it wouldn’t be because I had introduced a bug in the porting; it would be because the Swift Android runtime was doing something I didn’t expect.

Three weekends of focused work. Two of them productive.


What worked, almost embarrassingly

Pure-Swift code with no Foundation dependencies compiled and ran on the first try. Not first-day. First try. I copied ReviewOutcome.swift and SM2Scheduler.swift from the ThinkBud repo into a new Swift Package configured for Android, ran swift build --triple aarch64-linux-android24, and got a .so file out. Twelve seconds, zero modifications.

Calling that .so from Kotlin via the JNI shim Apple provides was rougher — about an hour of fighting JNI signature gotchas — but the Swift code itself required nothing. No conditional compilation. No #if os(Android). Nothing.

This was the moment the bet started feeling worth it. There’s a category of Swift code in my apps — algorithms, validators, parsers, model types — that is now genuinely portable to a second platform without rewriting in Kotlin. That’s not nothing.


What broke, and what I learned from it

The wins lasted about ninety minutes. Then Foundation.Date happened.

ThinkBud’s spaced-repetition algorithm reasons about “tomorrow at 6 AM in the user’s local time zone.” On iOS, Date and TimeZone are well-trodden territory — every WWDC since 2014 has touched them. On Swift Android in May 2026, TimeZone(identifier: "Europe/Belgrade") returns nil.

It just returns nil.

The Android runtime doesn’t ship with Apple’s full IANA time zone database. It ships with a subset — the major ones, the ones you’d use in a tutorial — and falls back to UTC for anything missing. The list of what’s included isn’t documented anywhere I could find. I ended up writing a small test that iterated through every IANA identifier and logged which ones returned nil; on my Pixel 8a emulator, about 40% of them did.

For a flashcard app where the entire premise is “your card is due tomorrow at 6 in the morning where you live,” this is not a small problem.

The fix wasn’t elegant. I bundled a trimmed copy of the IANA TZ database as a resource in the Swift Package and wrote a small PortableTimeZone type that uses Apple’s TimeZone on iOS and falls back to my bundled tables on Android. About 200 lines of code that does what Foundation should do. Six months from now I expect this is patched in the Android runtime and I delete the workaround.

That bug taught me more about real-world Swift Concurrency than the previous year of reading.

The reason: the PortableTimeZone had to be safe to use from any actor on Android and return correct results when crossing a 24-hour rollover and not block on disk I/O for the bundled tables. Three actor-isolation problems in a single 200-line type. I rewrote the actor topology four times before the test suite stayed green for an hour.

If you want to know whether you actually understand @MainActor, @concurrent, and nonisolated, port a date-handling type to a runtime where the underlying primitives might lie to you. It’s the most efficient way to find out where your mental model is fuzzy.


What surprised me

Three things, in increasing order of how much they surprised me.

Performance was within 10% of iOS for pure compute. I benchmarked the SM2 algorithm running 10,000 review calculations on a Pixel 8a vs an iPhone 16. Android took 11.4 ms, iOS took 10.6 ms. That’s basically noise. Swift on Android isn’t running through some translation layer — it’s compiled to native ARM64 with the same LLVM backend Apple uses. The fact that this works as well as it does is, frankly, the most impressive shipping engineering Apple has done all year.

Memory was the bigger gap. The same workload allocated 23% more peak memory on Android. Not because Swift is doing anything wrong — because the Android JNI bridge keeps Java references alive longer than equivalent Objective-C interop on iOS. If you’re porting memory-sensitive code, this is the dragon to plan around. I haven’t fully solved it; I’ve worked around it with explicit release() calls in the JNI shim layer.

Hot reload doesn’t work yet. This sounds minor. It isn’t. On iOS I can iterate on Swift business logic in 4 seconds via Xcode’s incremental build. On Android, every Swift change requires a full Swift Package rebuild + JNI re-link + Gradle re-package + APK reinstall. I measured: 47 seconds median, on a Mac Studio. That’s a 12x slower iteration loop. If you’re doing exploratory work, the friction is real and demoralizing. If you’re shipping known-good code, it’s fine. Plan accordingly.


Was it worth shipping?

I shipped a tiny Android build of “ThinkBud Review” — just the review screen, just for me, calling into the ported Swift core. Two friends are running it as a daily driver alongside the iOS version. After three weeks, the answer is: yes, but not for the reasons I expected.

The part I expected to value — “now I have one Swift codebase for two platforms” — didn’t pan out the way the marketing implied. I have one core for two platforms. The UI layer is still entirely separate (Kotlin on Android, SwiftUI on iOS), and that’s by far the largest part of any real app. The portable Swift slice is maybe 20% of ThinkBud by line count.

The part I didn’t expect to value — “now I’m forced to think harder about which code is actually business logic vs. UI logic vs. platform-coupled” — has changed how I architect new features. When I started adding the new “deck sharing” feature last week, I wrote the merge-conflict resolution logic in pure Swift, no import UIKit, no import Foundation.Calendar, just types and pure functions. I would have written it tangled with UI before.

The Android port made me a better iOS architect, even if I never actually ship the Android build to the Play Store.


Should you try it for your own app?

Honest matrix:

Your situationShould you try Swift on Android?
Your app is 80% UI, 20% business logicNo. The 20% won’t justify the operational cost.
Your app has a meaty algorithmic / data coreYes, the port might surface architecture you’ve been getting away with.
You want a real Android productYes, but plan for 4-6 months of UI rewrite in Kotlin. Swift only helps with the core.
You want to learn Swift Concurrency the hard wayAbsolutely. Date and TimeZone alone will teach you things WWDC won’t.
You’re hoping for “write once run everywhere”No. That’s not what this is, and pretending it is sets you up for disappointment.

If you’re in the second or fourth row, the SDK is genuinely ready enough to bet a weekend on. The IANA TZ gap and the JNI bridge friction are real but workable. The fact that pure-Swift compute just runs on Android in 2026 is, by itself, a thing I would not have believed a year ago.


The thing I’m still figuring out

I don’t know how to talk about ThinkBud-on-Android publicly yet. Apple’s framing is “Swift, everywhere,” which is a developer story. Mine is “I have an Android build, technically, that two friends use on the side.” Those don’t translate to App Store / Play Store positioning.

The honest version is something like: “I’m hedging against an iOS-only future for ThinkBud by keeping the core portable, but I’m not investing in an Android UI yet.” That’s not a marketing pitch — it’s a strategy memo to myself. I’ll know in six months whether to escalate.

For now, the Android port has earned its keep purely as an architectural forcing function. If you’re shipping iOS apps in 2026 and you’ve never been forced to think about which of your code is actually portable, three days of fighting Swift on Android will reveal more than a month of clean-architecture reading.

The IANA database workaround is in a public gist if anyone hits the same wall. I’ll link it in next week’s field note.

— Mario


Related — yesterday’s blog post on why I shipped 4 free iOS dev tools instead of marketing my apps explains the broader strategy this Android experiment fits into. And the original Swift 6.3 Android announcement coverage is here.

Share this post

Share on X LinkedIn

Comments

Leave a comment

0/1000

M

Mario

Founder & CEO

Founder of NativeFirst. Building native Apple apps with SwiftUI and a passion for great user experiences.