iOS Engineering

Declarative vs. Imperative: Understanding SwiftUI

The first line of every SwiftUI introduction inevitably mentions a “declarative framework.” React and Jetpack Compose also describe themselves as declarative.

4 min read
Cover image for Declarative vs. Imperative: Understanding SwiftUI

The first line of every SwiftUI introduction inevitably mentions a “declarative framework.” React and Jetpack Compose also describe themselves as declarative.

Yet when someone asks, “What does declarative mean?”, the answer is surprisingly elusive. “It means the code is clean” is not correct.

The difference between declarative and imperative comes down to one sentence. Do you write “How” or “What”?

This article uses everyday examples to establish the distinction, verifies it with UIKit and SwiftUI code, and explains why declarative programming is not free.

Here is the key summary.

  1. Imperative: give step-by-step instructions for reaching the desired result — How
  2. Declarative: describe the desired result and leave the procedure to the system — What
  3. SQL, HTML, map/filter, and SwiftUI are representative examples of declarative programming
  4. The essence of declarative UI: declare “when the state is this, the screen looks like this,” and the framework handles updates as the state changes

Think of it as a taxi ride

With the imperative approach, you give the driver directions yourself: “Turn right, go straight for 300 meters, turn left at the light…” You specify each step, and together those steps produce the result of reaching the destination.

The declarative approach is: “Please take me to Gangnam Station.” You state only the destination (What) and leave route selection (How) to the driver and the navigation system.

Both get you to the destination. The difference is whether I own the procedure or only the description of the result.


The same task can be expressed in code in two ways

Let’s write code that selects even numbers and squares them in both styles.

// Imperative: I specify how to iterate and where to store the results
var result: [Int] = []
for n in numbers {
    if n % 2 == 0 {
        result.append(n * n)
    }
}

// Declarative: I describe only what I want
let result = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

The imperative version exposes the “parts of the procedure,” such as loop variables, intermediate arrays, and ordering. The declarative version retains only the intent: “filter the even numbers and square them.” The iteration method is hidden inside filter and map.

You already use more familiar declarative forms. In SQL, you state only “give me rows matching these conditions,” not how to search the index. In HTML, you declare the structure—“a heading here, a paragraph here”—not the rendering procedure.


The difference becomes dramatic in UI

Imperative UI (UIKit) uses procedures for changing the screen.

// UIKit: Directly instruct screen updates whenever the state changes
func updateBadge(count: Int) {
    if count > 0 {
        badgeLabel.isHidden = false
        badgeLabel.text = "\(count)"
    } else {
        badgeLabel.isHidden = true
    }
}

The challenge is that developers must continuously track “what state the current screen is in.” When there are multiple update paths, it is easy to miss one, producing the classic bug: “the data changed, but the screen didn’t.”

Declarative UI (SwiftUI) declares what the screen should look like for a given state.

// SwiftUI: countDeclare that when the state is this, the screen looks like this
struct BadgeView: View {
    let count: Int
    var body: some View {
        if count > 0 {
            Text("\(count)").badgeStyle()
        }
    }
}

When count changes, I do not specify how to fix the screen. SwiftUI compares the previous and new declarations and updates only the necessary parts. UI becomes a “function of state.” Because ownership of the update procedure is delegated to the framework, there is no update code to forget.

Imperative code owns the update procedure; declarative code lets the framework calculate the difference
Imperative code owns the update procedure; declarative code lets the framework calculate the difference

Declarative programming is not free

For balance, we also need to examine the other side.

Hiding the procedure makes things frustrating when you actually need it. An imperative request like “move the scroll to exactly this offset” may require a workaround in a declarative framework.

Debugging has a different texture. With imperative code, you follow the procedure; with declarative code, you must trace the framework’s decisions, asking questions like “Why was this redrawn?” You cannot simply ignore the hidden How.

Performance tuning ultimately brings you back to understanding the internals. When SwiftUI recalculates views excessively, you need to understand how diffing and dependency tracking work to fix it.

The accurate perspective is therefore not “declarative is superior,” but a choice of abstraction level. Handing over ownership of the procedure simplifies code around intent, while leaving fine-grained control and internal understanding as challenges.

The trade-off: giving up fine-grained control in exchange for handing over ownership of the procedure
The trade-off: giving up fine-grained control in exchange for handing over ownership of the procedure

Summary

  • Imperative code instructs the procedure for reaching a result (How), while declarative code describes the desired result (What)
  • SQL, HTML, and map/filter are already familiar declarative forms
  • Imperative UI code specifies procedures for changing the screen and is prone to state–screen inconsistency bugs
  • Declarative UI declares “when the state is this, the screen looks like this,” and the framework handles updates (UI = function of state)
  • The cost of declarative programming: fine-grained procedural control is difficult, and understanding the framework’s internals is ultimately necessary
  • The essence is not superiority, but choosing who owns the procedure