Declarative vs Imperative UI
The Problem with Imperative UI
With UIKit, you imperatively tell the system how to build and update your interface. You create views, configure properties, add subviews, define constraints, and manually update state when things change. This creates a growing gap between what the UI looks like and the code that manages it.
// UIKit - Imperative approach
let label = UILabel()
label.text = "Hello, World!"
label.textColor = .blue
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
// Later, to update:
label.text = "Updated text"
label.textColor = .red
Every time the UI needs to change, you manually find the view and mutate its properties. Forgetting one update creates visual bugs. As screens grow, this imperative bookkeeping becomes a significant source of errors.
The Declarative Approach
SwiftUI flips the model. You write a function that describes what the UI should look like for a given state. When the state changes, SwiftUI automatically recomputes the view and updates the display.
// SwiftUI - Declarative approach
struct ContentView: View {
@State private var isRed = false
var body: some View {
Text("Hello, World!")
.font(.system(size: 18))
.foregroundColor(isRed ? .red : .blue)
.onTapGesture { isRed.toggle() }
}
}
You describe the desired state, not the steps to reach it. SwiftUI figures out the most efficient way to update the screen.
Why Declarative Matters
Declarative code is easier to reason about because the UI is a pure function of its state. Given the same state, you always get the same UI. This eliminates entire categories of bugs related to inconsistent view state, forgotten updates, and stale references.
Mental Model
Think of SwiftUI views as recipes, not constructions. A recipe describes ingredients and steps; it does not build the dish itself. Similarly, a SwiftUI view describes what the screen should look like, and the SwiftUI runtime handles the actual rendering and updates.
| Aspect | UIKit (Imperative) | SwiftUI (Declarative) |
|---|---|---|
| UI Definition | Code + Interface Builder | Swift code only |
| State Updates | Manual find & mutate | Automatic recomputation |
| Layout | Auto Layout constraints | Stacks, Grids, Frames |
| Previews | None built-in | Xcode Previews |
| Learning Curve | Steep | Gentle for beginners |
SwiftUI Architecture
The View Protocol
In SwiftUI, every view is a struct conforming to the View protocol. The protocol requires a single property: body, which returns some View. The body property describes the view's content and layout.
struct GreetingView: View {
var name: String
var body: some View {
VStack {
Text("Hello")
Text(name)
.font(.largeTitle)
}
}
}
Because views are structs, they are value types. This means SwiftUI can freely copy, compare, and discard them without worrying about reference semantics or memory management.
The View Graph
When SwiftUI encounters a view hierarchy, it builds a view graph -- a tree of view nodes that represent your interface. The graph is rebuilt when state changes, but SwiftUI is smart about which parts need updating.
struct ParentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
ChildView()
Button("Increment") { count += 1 }
}
}
}
struct ChildView: View {
var body: some View {
Text("I am a child")
}
}
When count changes, SwiftUI re-evaluates ParentView.body and determines that ChildView has not changed. It skips re-evaluating ChildView.body entirely, saving work.
Diffing and Updates
SwiftUI uses a diffing algorithm similar to a virtual DOM. When the view graph is re-evaluated, SwiftUI compares the new tree with the old one. It identifies insertions, deletions, and modifications, then issues the minimal set of updates to the underlying rendering system.
This diffing happens at the view boundary level. If a parent view's body changes but a child view's inputs are the same, the child is skipped. This is why struct-based views with stable identity are important for performance.
The Rendering Pipeline
- State change: A
@State,@Binding, or@Observablevalue changes. - Invalidation: SwiftUI marks the affected view as needing an update.
- Re-evaluation: The view's
bodyproperty is re-computed. - Diffing: The new output is compared with the previous output.
- Update: Only the changed parts of the underlying AppKit/UIKit layer are modified.
This pipeline is fast because SwiftUI optimizes each step. Views are structs (cheap to create), the diffing is targeted (only changed branches are compared), and the rendering uses platform-native primitives.
Identity and Stability
SwiftUI tracks views by their identity in the hierarchy. When views are in loops or conditionals, SwiftUI uses stable identifiers to match old and new views. This is why ForEach requires an id parameter:
ForEach(items, id: \.id) { item in
Text(item.name)
}
Without stable identity, SwiftUI cannot correctly match views across updates, leading to unexpected state loss or visual glitches.
SwiftUI vs UIKit
Coexistence, Not Replacement
SwiftUI does not replace UIKit -- it builds on top of it. Under the hood, SwiftUI views ultimately render through UIKit (on iOS) or AppKit (on macOS). This means you can mix SwiftUI and UIKit in the same project, which is essential for gradual adoption.
// Using UIKit inside SwiftUI
struct WebViewWrapper: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(URLRequest(url: url))
}
}
// Using SwiftUI inside UIKit
let hostingController = UIHostingController(rootView: MySwiftUIView())
When to Choose SwiftUI
SwiftUI excels for:
- New projects where you want a clean, modern architecture
- Simple to moderate UIs that map well to SwiftUI's component library
- Prototyping where rapid iteration with Xcode Previews is valuable
- Cross-platform apps targeting iOS, macOS, watchOS, and tvOS
When to Choose UIKit
UIKit remains strong for:
- Complex, highly custom UIs that push beyond SwiftUI's built-in components
- Performance-critical rendering with fine-grained control over the rendering pipeline
- Existing codebases with substantial UIKit investment
- Advanced interactions like custom gesture recognizers or complex animation choreography
Feature Comparison
| Feature | SwiftUI | UIKit |
|---|---|---|
| Declarative syntax | Yes | No |
| Previews | Built-in | Via third-party |
| Animations | Simple, integrated | Complex, manual |
| Accessibility | Automatic | Manual setup |
| Testability | Excellent (structs) | Moderate |
| Ecosystem maturity | Growing | Very mature |
| Platform support | iOS 13+, macOS 10.15+ | All versions |
Migration Strategy
Most teams adopt SwiftUI incrementally. Start with new screens or feature flags, wrap existing UIKit views in UIViewRepresentable, and gradually convert screens as confidence grows. The @main entry point can be a SwiftUI App struct that hosts UIKit view controllers through UIViewControllerRepresentable.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MainTabView()
}
}
}
This hybrid approach lets you benefit from SwiftUI's ergonomics while preserving your investment in battle-tested UIKit code.
Quiz
1. What is the primary difference between SwiftUI and UIKit?
2. Why are SwiftUI views defined as structs instead of classes?
3. Can SwiftUI and UIKit be used in the same project?
4. What is the role of the `body` property in a SwiftUI view?
5. What happens during SwiftUI diffing?
Flashcards
Question
What is declarative UI?
Click to reveal answer
Answer
A paradigm where you describe what the UI should look like for a given state, rather than imperatively telling the system how to build and update it.
Question
What protocol must every SwiftUI view conform to?
Click to reveal answer
Answer
The View protocol, which requires a body property returning some View.
Question
Why are SwiftUI views value types (structs)?
Click to reveal answer
Answer
Value types allow SwiftUI to cheaply copy, compare, and discard views for efficient diffing and view graph management.
Question
What wraps UIKit views in SwiftUI?
Click to reveal answer
Answer
UIViewRepresentable protocol, which bridges UIKit views into the SwiftUI view hierarchy.
Question
What is the SwiftUI rendering pipeline?
Click to reveal answer
Answer
State change -> View invalidation -> Body re-evaluation -> Diffing -> Targeted UI updates.
Revision Notes
Key Takeaways
- 1. Declarative UI describes what the UI should look like, not how to build it
- 2. SwiftUI views are structs with a body property returning some View
- 3. SwiftUI automatically diffs and updates only changed parts of the UI
- 4. SwiftUI and UIKit can coexist through bridging protocols
- 5. Understanding the view graph and diffing is key to writing performant SwiftUI
Interview Tips
- • Be ready to explain the difference between declarative and imperative UI paradigms
- • Know why SwiftUI uses structs (value semantics) instead of classes (reference semantics)
- • Understand the view graph lifecycle: state change -> invalidation -> re-evaluation -> diffing -> update
- • Be prepared to discuss when you would choose SwiftUI vs UIKit for a given project
Cheat Sheet
SwiftUI is declarative: describe UI as a function of state.
Every view is a struct conforming to View protocol with a body property.
SwiftUI builds a view graph, diffs old vs new, and applies minimal updates.
Coexist with UIKit via UIViewRepresentable and UIHostingController.
Views are value types for efficient copying and comparison.