5.1 — Philosophy & UIKit comparison

Opening scenario

You’re starting a greenfield app at a 40-person startup in 2026. Your team lead asks: “SwiftUI or UIKit?” Three answers will get you laughed out of the room:

  • “SwiftUI, it’s the future” — you’re picking the future, not what ships in 6 months.
  • “UIKit, SwiftUI isn’t ready” — that argument expired around iOS 16.
  • “It depends” without naming the dependencies — what does it depend on?

The right answer in 2026: SwiftUI by default, drop into UIKit for specific, named gaps — heavy custom collection views, mature third-party SDKs that ship UIView subclasses, or features that hit known SwiftUI limitations (custom keyboard handling, complex text editors, AVKit corner cases). Most production apps are mixed: SwiftUI hosting UIKit, UIKit hosting SwiftUI, sometimes in the same screen.

This chapter sets the mental model. The next 12 chapters teach you how SwiftUI actually works under the hood, so you can pick the right tool without superstition.

QuestionUIKitSwiftUI
Minimum deployment targetiOS 2.0iOS 13 (practical: iOS 16+ in 2026)
Programming paradigmImperative, object-orientedDeclarative, value-typed
Layout primitiveNSLayoutConstraint, Auto LayoutModifiers, Layout protocol
State to viewYou wire it manuallyFramework re-renders on change
MultiplatformiOS only (Catalyst for macOS)iOS, macOS, watchOS, tvOS, visionOS
Custom drawingUIView.draw(_:), Core AnimationCanvas, Shape, Path
AnimationBlock-based, CABasicAnimationwithAnimation { }, implicit
Maturity in 202618 years, battle-tested7 years, production-ready for most apps

Concept → Why → How → Code

Imperative vs declarative — the actual difference

Imperative (UIKit): You give the framework a sequence of instructions that do things — create a view, set its frame, add it as a subview, update its text when state changes. The framework executes; you are the choreographer.

// UIKit — imperative
class CounterVC: UIViewController {
    var count = 0
    let label = UILabel()
    let button = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = "Count: \(count)"
        button.setTitle("Increment", for: .normal)
        button.addAction(UIAction { [weak self] _ in
            guard let self else { return }
            self.count += 1
            self.label.text = "Count: \(self.count)"   // YOU update the view
        }, for: .touchUpInside)
        // ...add to hierarchy, constraints...
    }
}

You wrote label.text = "Count: \(self.count)" twice — once at setup, once in the action. Forget the second one, and the label stays at 0 while count ticks up. The bug class “UI out of sync with state” is the canonical UIKit defect.

Declarative (SwiftUI): You describe what the UI looks like as a function of state. The framework figures out what to render and what to re-render when state changes.

// SwiftUI — declarative
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
        }
    }
}

Text("Count: \(count)") is recomputed every time count changes. The framework owns the “when does the label get updated” question. You can’t write the bug above — there’s no place to put it.

The rendering model in one paragraph

When a @State/@Observable value mutates, SwiftUI marks the views that read it as invalid. On the next render pass, it calls body on each invalid view, which produces a new value-typed view graph. SwiftUI diffs the new graph against the previous one and only updates the underlying platform views (UIView/NSView) that actually changed. Your body returns a description — SwiftUI owns the rendering. Re-reading body is cheap; that’s why it’s safe to call thousands of times per second.

This is the same model as React, Flutter, Jetpack Compose. Apple just adapted it to Swift’s value types and Swift’s strong type system.

Where SwiftUI is great

  • Forms, settings, lists, navigation — declarative shines when the UI is a function of data
  • AnimationswithAnimation { state.x = 100 } is one line; the UIKit equivalent is 5-15 lines
  • Multiplatform — one codebase across iOS/iPadOS/macOS/watchOS/visionOS
  • Previews#Preview { } renders without launching the simulator (iterative speed gain measurable in hours/week)
  • Testability of view logic — your “ViewModel” is plain Swift, no UIViewController mocking
  • Accessibility defaults — VoiceOver, Dynamic Type, dark mode work out of the box

Where UIKit is still necessary

  • Complex UICollectionView layouts that need prefetchDataSource, UICollectionViewDelegateFlowLayout with hand-tuned cell heights, or interactive transitions
  • Custom text inputUITextField’s/UITextView’s delegate methods give finer control than SwiftUI’s TextField. Apple’s own Notes app uses UITextView.
  • Third-party SDKs that ship UIKit views — Mapbox, Stripe checkout, video player SDKs
  • Mature performance-critical screens — feeds with thousands of cells, video walls (Instagram Reels)
  • Specific gaps that vary by year — keyboard layout in chat apps, pull-to-dismiss sheets with non-trivial behavior, UIPageViewController parity
  • Custom drag-and-drop with complex previews — possible in SwiftUI, but UIKit’s API surface is mature

The 2026 production reality

Most apps you’ll work on are mixed:

  • New screens in SwiftUI, legacy screens stay UIKit
  • SwiftUI screen with one stubborn subview wrapped via UIViewRepresentable
  • UIKit UIViewController hosting a SwiftUI subview via UIHostingController
  • Modular architecture where each feature module picks its own framework

Examples:

  • Apple Notes (iOS 17+): SwiftUI shell, UIKit UITextView for the editor
  • Instagram: still mostly UIKit; SwiftUI for newer settings and onboarding flows
  • Airbnb: UIKit + their custom Epoxy framework; experimenting with SwiftUI for non-critical flows
  • Apple’s own Settings, Wallet, Reminders, Stocks: SwiftUI

When to pick what (decision tree)

Greenfield app, target iOS 16+:

  • Default to SwiftUI. Drop into UIKit for the screens where you hit a concrete blocker.

Existing UIKit app:

  • New screens: SwiftUI hosted via UIHostingController. Reusable UIKit components stay UIKit until rewrite makes business sense.

Multiplatform (iOS + macOS):

  • SwiftUI. Mac Catalyst is a maintenance pit; AppKit alone won’t share code.

Targeting iOS 15 or below:

  • SwiftUI is doable, but many modern APIs (NavigationStack, @Observable, Layout) require iOS 16+/iOS 17+. Evaluate per-feature.

watchOS or visionOS:

  • SwiftUI. Both platforms are SwiftUI-first.

Swift 6 + SwiftUI in 2026

SwiftUI in 2026 ships with:

  • @Observable macro (iOS 17+) — replaces ObservableObject/@Published for new code
  • Strict concurrency enabled — View.body is @MainActor-isolated
  • NavigationStack mature, NavigationView deprecated
  • SwiftData for persistence (Phase 6)
  • #Preview macro replaces PreviewProvider
  • Custom Layout protocol for bespoke layouts
  • MeshGradient, PhaseAnimator, KeyframeAnimator, scroll position APIs

If a tutorial uses NavigationView, ObservableObject, @Published, or PreviewProvider, it’s pre-2024 and there’s a more modern API. Read it for concepts, not boilerplate.

One concrete migration example

UIKit settings screen with one toggle, ~80 lines:

class SettingsVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var notificationsEnabled = UserDefaults.standard.bool(forKey: "notifications")
    let tableView = UITableView(frame: .zero, style: .insetGrouped)
    // viewDidLoad, register cell, dataSource, delegate, indexPath dispatching...
    // cellForRowAt: build cell, add UISwitch as accessoryView, addTarget for valueChanged
    // @objc func toggleChanged: write UserDefaults, no view update needed (switch owns state)
}

SwiftUI equivalent:

struct SettingsView: View {
    @AppStorage("notifications") var notificationsEnabled = false

    var body: some View {
        Form {
            Toggle("Enable notifications", isOn: $notificationsEnabled)
        }
    }
}

5 lines. Same behavior. @AppStorage handles UserDefaults read/write and view updates. This delta multiplied across every settings/form/list screen in an app is why teams are migrating.

In the wild

  • Apple’s own: SwiftUI is now the default for new Apple apps. Translate, Journal, Freeform are SwiftUI-heavy. Even the iOS Control Center editor in iOS 18 is SwiftUI.
  • Robinhood: started a partial SwiftUI migration in 2022; account/settings screens shipped first.
  • Lyft: uses SwiftUI for internal employee tooling apps and new onboarding flows; main rider/driver experience stays UIKit.
  • Stripe SDK: ships both UIKit and SwiftUI APIs because their customers use both.
  • Apollo (RIP): was SwiftUI-first in 2022 — one of the early “is SwiftUI production-ready?” proof points.

Common misconceptions

  1. “SwiftUI is for prototypes, not production.” That argument was valid in 2019–2021. In 2026, Apple ships their own multi-million-user apps in SwiftUI. The framework has bugs (every framework does), but they’re tractable.
  2. “SwiftUI is slower than UIKit.” Per-view, no. Some rendering paths are faster (less Auto Layout passes). Misuses (AnyView everywhere, computing huge collections in body) cause perceived slowness — that’s a misuse pattern, fixable.
  3. “You have to rewrite the whole app to use SwiftUI.” Wrong. UIHostingController and UIViewRepresentable let you mix at any granularity — screen, view, even modifier.
  4. “SwiftUI doesn’t give you fine-grained control.” It gives you less control over the render loop, by design. For everything else (custom layout, drawing, animations), there’s an escape hatch (Layout protocol, Canvas, UIViewRepresentable).
  5. @StateObject and @ObservedObject are the same thing.” They are not. @StateObject owns the instance (created once, survives view re-creation). @ObservedObject observes an instance owned elsewhere. Mix them up and you get state that disappears on every parent re-render — a real bug class.

Seasoned engineer’s take

I default to SwiftUI for any new screen in 2026. The leverage is real: the same code that takes me 80 UIKit lines takes 20 SwiftUI lines, and the SwiftUI version is testable without UIWindow instantiation. Where I push back on SwiftUI:

  • Lists with 10K+ items and complex cellsUICollectionView with compositional layout still wins on memory and scroll performance for the heaviest cases.
  • Rich text editors — SwiftUI’s TextEditor improved a lot but UITextView + NSAttributedString is still the path for custom typography.
  • When my team has zero SwiftUI experience and we ship in 4 weeks — learning curve cost matters; use what people know.

When I review a SwiftUI codebase, the bug classes I look for are: stale closures capturing initial state, AnyView erasure killing diffing, @StateObject initialized from a parent prop (anti-pattern), and body doing work (network calls, mutating state outside onAppear/task).

TIP: When you’re learning SwiftUI, write the same screen twice — once in UIKit, once in SwiftUI. Side-by-side gives you intuition for what each framework optimizes for. After ~5 of these, you stop debating and start picking.

WARNING: “We’ll migrate to SwiftUI over the next year” is a project-killer if there’s no concrete per-screen plan. Migration without a deadline is renaming things. Pick the screens, pick the order, pick the kill-switch, ship.

Interview corner

Junior-level: “What’s the difference between declarative and imperative UI?”

Imperative: you tell the framework how to update the UI step by step (label.text = "new value"). Declarative: you describe what the UI is as a function of state (Text(value)), and the framework figures out when to re-render. SwiftUI is declarative; UIKit is imperative.

Mid-level: “You’re starting a new iOS app targeting iOS 17+, 4-person team, 6-month timeline. SwiftUI or UIKit, and why?”

Default to SwiftUI. Two of four engineers can ramp on it fast; iteration speed (previews, less boilerplate) gives back days. Identify likely escape-hatch screens upfront: any chat UI, media-heavy feeds, anything with mature third-party UIKit SDKs (Stripe SDK, video player, advanced map). Set a convention: those screens use UIViewControllerRepresentable. Acknowledge SwiftUI’s costs: smaller talent pool with deep experience, occasional framework bugs (have workarounds in mind). Six months at four engineers is enough to ship a production SwiftUI app.

Senior-level: “Walk me through how SwiftUI’s diffing actually works. What’s the cost model? When does it fall down?”

SwiftUI builds an immutable value-typed view graph from body. Each view has an identity derived from its position in the graph plus any explicit id modifier. On state change, SwiftUI re-invokes body on the affected branches, produces a new graph, walks it in parallel with the old graph, and computes a minimal diff. Identical view structs (same type at the same position) get their underlying UIView/NSView reused with new properties applied; structurally different views are torn down and rebuilt.

Cost model: body invocation should be O(constant) per view. Diffing is O(graph size). Where it falls down: erasing types to AnyView (defeats the static type-based fast path; SwiftUI falls back to dynamic diffing), reading state in deep ancestors (invalidates large subtrees), building giant view bodies inline (compiler timeout), creating objects in body (allocating per invalidation, plus closures capturing state on every render). Fixes: prefer typed some View, push state ownership down to leaves, extract subviews, use EquatableView for expensive subtrees with custom equality.

Red flag in candidates: Claiming you’ve “shipped SwiftUI in production” but can’t articulate the difference between @StateObject and @ObservedObject, or doesn’t know what body is called on. Indicates copy-paste fluency without mental model.

Lab preview

Phase 5 labs build up from a SwiftData todo app (Lab 5.1) through animated dashboards (Lab 5.2), a true multiplatform app (Lab 5.3), and end with a reusable component library shipped as a Swift Package (Lab 5.4). By the end you’ll have made the SwiftUI-vs-UIKit decision dozens of times under realistic constraints.


Next: Views, modifiers & the rendering model