Back to Blog

SwiftUI in Production: What We'd Tell Ourselves at App #1

We've written about why we build native instead of cross-platform, but "native" still leaves a real choice: SwiftUI or UIKit. We've built our whole portfolio in SwiftUI, and it's been the right call — but not for the reasons we expected going in, and not without real scar tissue along the way. This is the honest version, not the keynote version.

Previews are the actual productivity win

Not the declarative syntax, not the reduced line count — the Xcode canvas. Being able to see a screen update live while editing, across multiple device sizes and both color schemes simultaneously, cuts the build-run-tap loop out of a huge share of UI work. On apps with a lot of screens per feature — our Legi & Amenzi reference apps, the Civio quiz engines — that loop-time saving compounds fast. It's the single biggest reason SwiftUI apps ship faster for us than UIKit ones did.

State management is where the real learning curve lives

The framework's data-flow story evolved fast across iOS releases — from @State/@ObservedObject to @StateObject to the newer @Observable macro — and each shift genuinely changed how you should structure a view hierarchy, not just the annotation you type. Apps we started earlier carry patterns from an older era of that story; the honest fix isn't a full rewrite, it's migrating module by module as you touch each one, which is slower to look finished but far cheaper than a big-bang rewrite that stalls other work.

Lists are the first place performance actually bites

A List or ScrollView full of dynamic content — exam questions in Civio, workout entries in Gym Tracker, maintenance records in My Mustang — is where SwiftUI's diffing can start doing more recomputation than expected if the view identity isn't set up deliberately. Explicit, stable identifiers on list items and keeping row views cheap to re-render solves most of it. This almost never shows up on a simulator with test data; it shows up on a real device once a user has months of real history in the app, which is exactly why it belongs in the pre-submission checklist, not just initial QA.

Navigation got better, late

NavigationStack replacing the older NavigationView fixed a real category of bugs — state getting lost or views double-pushing on programmatic navigation — that used to require workarounds. Apps built before that transition needed a deliberate migration, not a drop-in swap, because the old and new navigation models don't compose cleanly. It's a good example of why we keep a minimum supported iOS version per app instead of chasing every user on every legacy release: some framework changes are worth requiring the newer OS for.

Where we still reach for UIKit

Not everything is a SwiftUI-only decision. A handful of interactions — fine-grained gesture recognizers, some audio and haptic timing work, a couple of custom animation curves — are still easier or more precisely controllable via UIViewRepresentable bridges into UIKit than through pure SwiftUI APIs. That's not a framework failure; it's knowing which 5% of a screen is worth dropping down a level for, instead of forcing everything through one paradigm on principle.


None of this is an argument against SwiftUI — quite the opposite, it's why every app in our portfolio uses it. It's an argument for treating "we use SwiftUI" as the start of a real architecture decision, not the end of one.

Mădălin Săvoaia Founder & iOS Engineer, ISTACK DEVELOPMENT

Inheriting a SwiftUI codebase that's started to creak?

We can audit the state management and navigation layer before it turns into a rewrite.