Journal · Apr 21, 2026

SwiftUI in year five: what we still reach for UIKit for

After five years of shipping SwiftUI as our default, the honest list of where UIKit still wins — and the interop patterns that keep a mixed codebase from becoming a mess.

We've been shipping SwiftUI as our default iOS stack for about five years now, and the framework we use today is barely recognizable from the one we cautiously adopted. NavigationStack fixed navigation. The @Observable macro fixed the invalidation model — views now update only when the specific properties they read change, which quietly eliminated a whole class of performance mysteries. Swift 6 concurrency checking catches the data races we used to find in crash reports.

So let's be clear up front: SwiftUI is the default. Every new screen starts there, and most of them end there. But "default" and "always" are different words, and clients deserve the honest version of where the line actually sits in 2026. Pretending UIKit is legacy is as wrong as pretending SwiftUI isn't ready. Both takes ship worse apps.

Where SwiftUI is now simply better

It's worth stating what's no longer a debate, because the list has grown long:

  • Standard screens. Settings, forms, detail views, onboarding, dashboards — the 80% of any app. SwiftUI does these in a fraction of the code, and previews make iteration absurdly fast.
  • State-driven UI. The declarative model means whole categories of bugs — the stale cell, the view that didn't hear about the update — structurally can't happen.
  • Lists that used to be scary. LazyVStack and List handle the overwhelming majority of scrolling content fine now. The old advice to drop to UITableView for anything long is mostly obsolete.
  • Anything multiplatform. Widgets are SwiftUI-only. watchOS is SwiftUI-only in practice. visionOS was born SwiftUI-native. Sharing view code across iPhone, iPad, and Watch is a real economic advantage, not a slide-deck claim.
  • Animation. The Animation APIs, matchedGeometryEffect, and spring physics defaults produce work that used to require a specialist.

If a contractor proposes UIKit for a greenfield form-and-list app in 2026, ask why. The honest answers are rare.

The honest UIKit list

Here's where we still deliberately reach for UIKit, on real projects, this year.

Heavy custom collection layouts. UICollectionViewCompositionalLayout with diffable data sources remains the strongest layout engine on the platform. For feeds with mixed cell types, orthogonal scrolling sections, and per-section layout logic — the "app store front page" pattern — it out-muscles SwiftUI's lazy containers, and cell recycling still handles memory on enormous datasets more predictably than lazy stacks.

Serious text editing. TextEditor covers notes-app basics. The moment requirements say rich text, custom attributes, inline attachments, find-and-replace, or precise selection control, we're in UITextView territory — usually with TextKit 2 underneath. Text is decades of accumulated edge cases; UIKit has the decades, SwiftUI doesn't yet.

Camera and media pipelines. AVFoundation preview layers, custom capture UI, real-time filters — this is UIViewRepresentable wrapping a UIView whose layer is an AVCaptureVideoPreviewLayer, full stop. The same goes for anything that needs CADisplayLink-driven drawing or tight control over CALayer trees.

Fine-grained scroll control. SwiftUI's scroll APIs improved enormously — scrollPosition, onScrollGeometryChange, visibility callbacks — and they cover most product asks now. But interactions built on continuous scroll physics (custom paging with interruptible snapping, scrub-linked transitions, collapsing headers with pixel-exact choreography) still land in UIScrollView delegate methods, where you can interrogate and redirect momentum mid-gesture.

Interruptible, gesture-driven transitions. The card that tracks your finger, cancels mid-flight, and hands momentum back — UIKit's transition machinery and UIViewPropertyAnimator still model "the user changed their mind mid-animation" better than anything SwiftUI exposes.

That's the whole list, and it's shrinking by roughly one item per WWDC. But notice what's on it: text, cameras, scroll physics. The stuff at the physical edges of the platform, where thirty years of accumulated API surface actually earns its complexity.

Interop that keeps a codebase sane

A mixed codebase isn't a compromise — it's the intended design. Apple ships interop hooks in both directions and improves them yearly. What separates a sane hybrid app from a haunted one is discipline about the boundary.

Rule one: one-way data flow across the bridge. A wrapped UIKit component receives state and reports events. It never becomes a second source of truth.

struct CameraPreview: UIViewRepresentable {
  let session: AVCaptureSession

  func makeUIView(context: Context) -> PreviewView {
    let view = PreviewView()
    view.videoPreviewLayer.session = session
    return view
  }

  func updateUIView(_ uiView: PreviewView, context: Context) {}
}

The SwiftUI side owns the session's lifecycle; the representable just renders it. Every time we've let a wrapped view own state, we've eventually paid for it in "who updated whom" debugging.

Rule two: keep updateUIView idempotent and cheap. SwiftUI calls it whenever it likes. Diff against current state before touching the view, or you'll trigger layout storms you can only see in Instruments.

Rule three: embed at the largest sensible granularity. Wrap a whole camera screen, not five camera widgets. Every bridge crossing is a place where layout, animation, and focus behavior can disagree; fewer, bigger crossings mean fewer seams to caulk. Going the other direction, UIHostingConfiguration lets SwiftUI render cell content inside a compositional-layout collection view — which is exactly how we build those heavy feeds: UIKit owns the scrolling and recycling, SwiftUI owns everything inside each cell. It's the best of both, officially sanctioned.

Rule four: contain the legacy. In older codebases we modernize, UIKit lives in clearly-marked modules with SwiftUI facades. New feature work never imports UIKit directly; it imports the facade. That keeps the migration moving in one direction instead of oscillating.

How we actually decide

The decision rule we use on every screen is unglamorous: start in SwiftUI; drop to UIKit when a named requirement demands it — never on vibes. "SwiftUI can't do this" has to come with the specific API gap written down, because half the time the gap closed two OS versions ago and the folklore outlived the fact.

We also re-audit the folklore annually, after each WWDC. Items fall off the UIKit list every year — scroll APIs came off it recently; long lists came off years ago. A team whose UIKit list hasn't shrunk since 2022 isn't being careful. It's being nostalgic.

Five years in, that's the whole story: SwiftUI for the app, UIKit for the edges, a disciplined bridge between them, and a standing appointment every June to move the line.