The pitch for cross-platform mobile development is irresistible: write your app once, ship it to both iPhone and Android, never build the same screen twice. I believed it. Then I spent a couple of years learning, the hard way, that “cross-platform” is a spectrum — and that most of the popular ways onto it quietly hand you two problems in a trench coat pretending to be one.
This is the honest version of that road: what went wrong with the native-first approaches I tried, the wall that has nothing to do with code at all, and why I eventually threw it all out for React Native and Expo.
The two-headed native approach
I started where a lot of people start: as close to native as possible, on both sides. Android in Kotlin, the Apple side in C# through the .NET and MAUI world. On paper it’s the “serious” choice — real native performance, full access to the platform, none of that flimsy webview stuff. In practice it meant living in two toolchains, two languages, and two mental models at once, and paying a tax on every single feature.
Every screen got built twice. Every piece of business logic either got duplicated or wedged into some shared layer that fought both platforms equally. A change that should have taken an afternoon took two — because it took two, once in Kotlin and once in C#, plus the time lost swapping my brain between them. And the tooling was never quite there: MAUI in particular felt like it was maturing underneath me while I was trying to ship on it, the kind of experience where half your bugs are yours and the other half are the framework’s and you can’t always tell which. I wasn’t building an app. I was maintaining two apps that happened to share a name.
Then the wall: iOS needs a Mac
Here is the part nobody warns you about loudly enough, and it has nothing to do with which language you picked. To compile, sign, and submit an iOS app, Apple requires macOS and Xcode. Full stop. Not “it’s easier on a Mac” — the final iOS build, and the code-signing that App Store submission depends on, run on Apple’s toolchain, on Apple’s operating system, on Apple’s hardware. If you don’t own a Mac, you’ve hit a wall no amount of clever coding gets you through.
And I didn’t own a Mac. So there I was with an app that ran fine on Android and an iOS half I could write but not actually ship — couldn’t do a real device build, couldn’t sign it, couldn’t put it in front of a TestFlight tester, couldn’t submit it. The most expensive part of “cross-platform” turned out not to be the code. It was a hardware requirement gatekeeping the entire second platform.
Switching to React Native and Expo
I moved to React Native almost out of exhaustion, and the first thing that changed was the obvious one: one codebase, in TypeScript, for both platforms. The endless duplication just… stopped. One component rendered on both. Logic lived in one place. The ecosystem was enormous, so most problems already had a library and an answer instead of a framework bug I’d file and wait on.
But the thing that actually solved my problem was Expo — specifically its cloud build service. Because Expo lets you build your iOS app on their Mac infrastructure, in the cloud, without ever opening Xcode or owning a single Apple device.
How you ship iOS with no Apple hardware
The workflow is almost anticlimactic. You develop and test on Android and in Expo’s dev client on a real iPhone over Wi-Fi. When it’s time for a real build, you run eas build --platform ios and Expo’s cloud — their Macs — compiles and signs it for you. Then eas submit pushes it to App Store Connect and TestFlight. The Mac requirement never went away; Apple still demands one. Expo just rents you theirs, per build, so you never have to buy one.
That single capability — EAS Build handling the iOS compile-and-sign on cloud Macs — is the whole difference between “I have an Android app and an aspiration” and “I have an app on both stores.” It removed the exact wall the native approach had built and couldn’t get me over.
The honest tradeoffs
None of this is free, and I’d be lying to call Expo a pure win. You’re a layer up from the metal, so a genuinely exotic native need can mean writing a config plugin or dropping to a bare workflow, which claws back some of the simplicity. Certain platform-specific debugging is still easier if you do have a Mac and Xcode within reach. And you’re now leaning on a build service — a real dependency, with its own queue times and pricing, that you should choose with open eyes.
But weigh that against the alternative I’d actually lived: two codebases, two languages, a framework maturing underneath me, and a hard hardware wall between me and half my users. The tradeoff wasn’t close. For a solo developer or a small team — especially one without a room full of Apple hardware — Expo trades a little ceiling for an enormous amount of floor.
The lesson underneath the tooling
The real takeaway isn’t “Kotlin and C# are bad” — they aren’t; in the right context they’re exactly right. It’s that the best tool is usually the one that removes the most friction between you and a shipped product — and that friction is often not where you expect. I spent ages optimizing the wrong variable, language and performance, while the thing actually stopping me was a build-and-signing requirement a cloud service could absorb completely. Choose for the wall you’re actually stuck at, not the one the “serious” advice tells you to worry about.
We build cross-platform mobile apps end to end — you can see a couple on our apps page — including shipping to the App Store without a rack of Apple hardware. That’s what we do at Rebel Studios.