React Native vs Native iOS: An Honest Decision Guide

A practical React Native versus native iOS decision guide from shipping food logging, travel, audio, image editing and WidgetKit apps in real production.

A 2.0.3 React Native app sat beside a 2.0.0 Swift app and a 1.0.0 Kotlin app in the same food-tracking repository, which looks like indecision but was the useful part.

The React Native app had the same iOS and Android package identifiers, an Expo Application Services production profile and submission configuration, while the Swift and Kotlin apps talked directly to a Cloudflare Worker and kept data locally, and React Native described a Rails-backed food-entry serialisation contract.

React Native versus native iOS is not a library comparison. The question is what carries the risk, and whether an extra client boundary costs less than two implementations.

I have shipped both: a food logger and travel journal did not need all product logic in Swift, while a live audio meter, on-device image editor and child-health log with WidgetKit had specific reasons to be native, because native does not feel more serious and one codebase does not make React Native efficient.

React Native wins when the product is mostly an application boundary

Food logging contains enough device work to make the choice non-obvious: a person can type a meal, photograph it or scan a barcode, expects entries, fasting, water and fitness history to survive a train tunnel, and may pay, so separate what touches the phone from what defines the product.

The contract is the meal entry: a description or picture becomes calories, protein, carbohydrates, fat and named items, the user reviews the estimate before saving, and barcode input reads public per-100 gram nutrition fields from Open Food Facts; the hard work is exposing an uncertain estimate and storing the chosen record, not 120 frames-per-second graphics.

The React Native and Expo client is the strongest evidence for the cross-platform route, with the highest declared version, 2.0.3, production EAS build and submission configuration, and the same iOS and Android package identifiers, which signals a likely shipped client rather than release proof because configuration can be ready for a release that never happens.

SwiftUI uses SwiftData and Android uses Room, with both calling a Cloudflare Worker directly, while React Native names a Rails entry serialisation format, which suggests but does not prove a transition from a direct Worker boundary to Rails as source of truth; text and image meals return structured fields, and React Native fits when that agreement is central, independently testable, and each client turns it into local state.

The iOS client creates or restores a device UUID, adds platform and App Attest headers, compresses a photograph towards 1 MiB before base64 encoding, times out requests and retries a retryable server failure once after two seconds, while Android mirrors the contract and keeps its device identity; the Worker allows attested callers 100 AI requests daily and other callers 10, with a 25-second upstream timeout.

Camera input, a device identifier, local persistence, purchases, notifications and secure storage can all have React Native implementations, although each needs an owner when Apple or Android changes and a real-device release check, because shared UI is cheaper only while the native edges stay thin.

For an account-based product, they often do: forms, search, feeds, saved objects, server-backed settings, subscriptions, photo selection and ordinary push notifications do not improve because the screen is Swift, so keep domain types in TypeScript, make one API contract boring and spend saved time on what users notice.

The travel journal has a related shape: Rails owns journeys, visits, places, memory pins, photos, permissions, map providers and private file delivery, while its iPhone app is a native UIKit shell around the same Hotwire interface, bridging camera, photo-library access and on-demand location; the bridge asks only for when-in-use permission, errors when denied, rounds coordinates to two decimal places and exposes no better than 1,000 metres of accuracy, so a cross-platform client could work too, but the choice is a rich, separate runtime or three defined bridges and a reliable web product.

Native iOS earns its cost at the real-time boundary

The sound meter starts an AVAudioEngine input tap in measurement mode, receives 2,048-frame PCM buffers, calculates an A-weighted dB(A) estimate from live audio, processes buffers in memory, updates the display and tears down the audio session when measurement stops.

Its signal path applies a Hann window, complex forward DFT, IEC 61672 A-weighting curve per frequency bin, inverse DFT, corrected RMS and 10 × log10(meanSquare), with silence floored at -120 dBFS; the view model applies an exponential moving average of 0.25, maintains session min, mean and max, holds a decaying peak and warns after 30 seconds above 85 dB.

React Native can do this with native modules, but that means Swift audio processing behind JavaScript plus lifecycle, permissions, buffering and diagnostics across that interface; if Android is required, that can be worthwhile, but for an iPhone-only product whose value sits inside the audio callback, keep the performance path in Swift.

The sound meter has 34 unit-test methods across six files, covering invalid FFT sizing, a full-scale 1 kHz sine, 100 Hz A-weight attenuation, silence, curve points, calibration clamping, access state and trial expiry; UI tests skip live microphone capture and verify screen flow rather than claiming an emulator proved acoustic accuracy.

A phone microphone reports dBFS, not calibrated sound pressure, so the app offers an estimate, uses a documented 120 dB offset and permits calibration plus or minus 10 dB against a reference meter; native did not make it a professional instrument, but it made the limitation easier to expose, and for a live signal path, on-device model loop, AR, low-latency video, custom camera processing, complicated extension or physical accessory protocol, start native and use the debugger, memory tools and first-party API surface where timing matters.

Image processing is native when memory is part of the feature

The photo editor lets a person tap multiple subjects, make each a transparent layer, move, scale, rotate and flip them, retouch with a soft mask, replace the background and export, because a photo picker is not the hard part.

It uses Apple's on-device subject lifting through ImageAnalyzer and ImageAnalysisInteraction, redraws a tapped cutout onto a transparent full-frame canvas and preserves the original framing, while Core Image handles blur, pixelation and masked blending and UIKit source-over drawing composes ordered layers.

Inputs are orientation-normalised and capped to a 3,072-pixel longest edge at scale 1, below the 8,192-pixel Vision limit and without a 3 times device-scale expansion; the editor uses 1,600-pixel previews and exports at the prepared canvas size, caches effects as display cutouts and rebuilds them only when an effect changes, while the preview does not bake a large bitmap on every pinch or drag and waits for gesture end, a layer change or a background change.

The value is iOS image frameworks, alpha-aware pixel formats and memory control, so JavaScript UI would not reduce enough native work to pay for another boundary and would make profiling harder; image size, renderer behaviour, transparency and export fidelity are the risk, and the source converts opaque Core Image output to noneSkipLast RGB, removing unused alpha and ImageIO warnings.

Apple subject lifting is the feature, so if Android needs equivalent quality, budget a separate Android technical investigation before promising parity, because a shared codebase does not prove both image pipelines have the same output.

WidgetKit changes the shape of a small app

The child-health log has profiles, temperature readings, symptoms, medicines, notes, photos, appointments, vaccines and growth records, and while a cross-platform screen stack could handle much of it, WidgetKit makes it an extension system.

The app has a Core Data model with nine entities, optional CloudKit synchronisation through private and shared stores, and a child as sharing root, while the widget never opens that database; after launch, focused-child changes and Core Data saves, the app recalculates a compact WidgetSnapshot, atomically writes widget-snapshot.json to a shared App Groups container and asks WidgetKit to reload timelines, and the extension reads only that snapshot.

An extension is a constrained process, so the widget must show an active episode, latest temperature, dose-gap state and last medicine without creating another source of truth; it stores next-dose state as a date rather than a pre-rendered relative string, allowing WidgetKit to move from "OK after" to "OK now" at the right timeline point, and schedules an entry at the dose interval end with a 30-minute fallback refresh.

React Native can coexist with native widgets, but when a widget is central to retention or safety, its data contract, privacy markings, deep links, refresh rules and failure mode need native ownership, so do not call it a minor integration and then discover an unbudgeted native subsystem.

Its App Store Connect workflow stages metadata, archives and exports a build, uploads it, polls for registration, attaches it to a version and leaves review submission manual, so an automated upload is not a shipped release; StoreKit, CloudKit and widgets require checks outside a TypeScript test suite and sometimes the simulator.

The release path decides more than the code generator

The old title mentioned Ignite and Hetzner, but I found no evidence that I used either for these products, so I will not turn them into personal practice; they may be good tools, but that is a different claim.

Choose the release path with the architecture: Food & Fit has EAS production build and submission configuration, which makes the route explicit but is not an acceptance test, so check a release binary against the real backend, camera permissions, purchase configuration, deep links, notification payloads and platform-specific failures, because over-the-air JavaScript updates cannot fix a missing entitlement, broken native dependency, bad privacy declaration or StoreKit issue in a binary under review.

The iOS food client compiles a production Worker URL into the release build, sends platform and App Attest headers, changes image payloads before they leave the phone, and keeps saved entries offline while new AI analysis and barcode lookup do not, so test that on a release-like build with an account and bad connection, not only a simulator.

For the travel journal, Rails verifies that a blob belongs to a photo attached through the signed-in user's own trip, so unauthenticated and non-owner requests get not-found; the iOS shell must exercise the same session behaviour as the browser, or it can open a wrong URL or leak a photo after authentication expires.

The child-health repository still requires two-device, two-Apple-ID sharing verification before production reliance, because CloudKit can compile and tests can cover the local snapshot while sharing fails at the account boundary, so leave that uncertainty visible.

My checklist is architectural: what data is local, what state is a server contract, which extension reads it, which entitlement gates it, and what can change after review versus what needs a binary, so test the paths that cross those lines.

If you are deciding how to build an MVP, the cost is mostly in the decisions you have not made, while the integration checklist covers the checks I expect around an AI-service boundary.

I build iOS, React Native and server-side products, and I choose the client around the hard part rather than the fashionable part. My CV and rates are here.

Related