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.