---
title: 'Previews and Iteration'
source: 'https://academia.sh/en/courses/swiftui/previews-and-iteration'
course: 'UI Development with SwiftUI'
language: en
updated: '2026-10-21T05:03:13+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains that a preview gives a view a sample model and overridden environment values'
    - 'Measures that when a sample model property changes, the view that reads it is recomputed through the observed trigger'
    - 'Shows that opening a preview is a new starting state, not a change'
    - 'Reads the trigger distribution of the twenty-one lessons in the course from a table'
---

# Previews and Iteration

Showing how a preview recomputes a view through its sample model and overridden environment, why opening a preview does not count as a change, and a summary of the twenty-one lessons in the course.

In the previous lesson we measured that the locale and the layout direction cause recomputation through the environment trigger. In this lesson we look at the fast feedback loop: seeing a view on its own, without running the whole app. The question is again the same: **what triggered this recomputation?** The trigger examined in this lesson is **observed**.

A preview builds a view by itself. If the view needs a model, the preview gives it a sample model instead of real data. The preview can also determine the environment values the view reads; for example, it can force the dark color scheme. These two paths match the two triggers we measured earlier: a property of the sample model is observed, and an overridden environment value is the environment trigger. The preview itself is the work of a development tool and is not measured in this lesson; what is measured is the values the preview gives to the view.

The measurement uses the core file; the core arrives in every lesson with the same file name and the same content.

```swift
// Invalidation.swift — classifies which view a change recomputes and through which source
enum Storage {
    case state(owner: String)
    case model(object: String)
    case environment
}

struct ViewNode {
    let name: String
    let parent: String?
    let reads: Set<String>

    init(_ name: String, parent: String?, reads: Set<String>) {
        self.name = name
        self.parent = parent
        self.reads = reads
    }
}

struct Recompute {
    let view: String
    let cause: String
}

struct ViewTree {
    let nodes: [ViewNode]
    let storage: [String: Storage]

    func ancestors(of name: String) -> [String] {
        var found: [String] = []
        var current = nodes.first { $0.name == name }?.parent
        while let next = current {
            found.append(next)
            current = nodes.first { $0.name == next }?.parent
        }
        return found
    }

    func cause(of node: ViewNode, key: String) -> String {
        guard let kind = storage[key] else { return "out-of-scope" }
        switch kind {
        case .environment:
            return "environment"
        case .model:
            return "observed"
        case .state(let owner):
            if owner == node.name { return "local" }
            if ancestors(of: node.name).contains(owner) { return "parent" }
            return "off-rule"
        }
    }

    func change(_ label: String, key: String) -> [Recompute] {
        let hits = nodes.filter { $0.reads.contains(key) }
            .map { Recompute(view: $0.name, cause: cause(of: $0, key: key)) }
        let skipped = nodes.filter { !$0.reads.contains(key) }.map(\.name)
        let recomputed = hits.isEmpty ? "none" : hits.map { "\($0.view) \($0.cause)" }.joined(separator: ", ")
        let rest = skipped.isEmpty ? "none" : skipped.joined(separator: ",")
        print("\(label): \(key) changed | recomputed \(recomputed) | skipped \(rest)")
        return hits
    }
}

func countCauses(_ recomputes: [Recompute]) {
    for cause in ["local", "parent", "observed", "environment"] {
        print("\(cause): \(recomputes.filter { $0.cause == cause }.count)")
    }
}
```

## Sample Model and Overridden Environment

The fragment below is a profile screen. The name and the status are properties of an observable model. The avatar reads the color scheme from the environment. The two preview declarations build the same screen differently: one writes a short name into the model, the other turns the color scheme to dark.

```swift
import SwiftUI

@Observable
final class ProfileStore {
    var displayName = ""
    var status = ""
}

struct ProfileScreen: View {
    let store: ProfileStore

    var body: some View {
        VStack {
            AvatarView()
            NameLabel(store: store)
            StatusBadge(store: store)
        }
    }
}

struct AvatarView: View {
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        Circle().fill(colorScheme == .dark ? Color.white : Color.black)
    }
}

struct NameLabel: View {
    let store: ProfileStore

    var body: some View {
        Text(store.displayName)
    }
}

struct StatusBadge: View {
    let store: ProfileStore

    var body: some View {
        Text(store.status)
    }
}

#Preview("Short name") {
    let store = ProfileStore()
    store.displayName = "Ada"
    return ProfileScreen(store: store)
}

#Preview("Dark") {
    ProfileScreen(store: ProfileStore())
        .environment(\.colorScheme, .dark)
}
```

This fragment does not compile and produces no output; it stands as source only. The reading layout is as follows. `ProfileScreen` reads no value directly; it passes the model to its children but does not touch its properties. `NameLabel` reads `displayName`, `StatusBadge` reads `status`, and `AvatarView` reads `colorScheme`.

We write three changes. The first two are properties of the sample model: the name and the status. The third is a change of the color scheme that the preview overrides.

```swift
// main.swift — sample model and overridden environment
let storage: [String: Storage] = [
    "displayName": .model(object: "ProfileStore"),
    "status": .model(object: "ProfileStore"),
    "colorScheme": .environment,
]

let preview = ViewTree(nodes: [
    ViewNode("ProfileScreen", parent: nil, reads: []),
    ViewNode("AvatarView", parent: "ProfileScreen", reads: ["colorScheme"]),
    ViewNode("NameLabel", parent: "ProfileScreen", reads: ["displayName"]),
    ViewNode("StatusBadge", parent: "ProfileScreen", reads: ["status"]),
], storage: storage)

let recomputes = [
    preview.change("sample-name-changed", key: "displayName"),
    preview.change("sample-status-changed", key: "status"),
    preview.change("overridden-mode-changed", key: "colorScheme"),
].flatMap { $0 }

countCauses(recomputes)
```

```
sample-name-changed: displayName changed | recomputed NameLabel observed | skipped ProfileScreen,AvatarView,StatusBadge
sample-status-changed: status changed | recomputed StatusBadge observed | skipped ProfileScreen,AvatarView,NameLabel
overridden-mode-changed: colorScheme changed | recomputed AvatarView environment | skipped ProfileScreen,NameLabel,StatusBadge
local: 0
parent: 0
observed: 2
environment: 1
```

When the sample model's name changes, only `NameLabel` is recomputed, and the trigger is observed. When the status changes, only `StatusBadge` is computed. When the overridden color scheme changes, only `AvatarView` is computed, and the trigger is environment. `ProfileScreen` is skipped in all three changes, because it only passes the model on and reads none of its properties. The final count gives two observed and one environment recomputation; the local and parent counts are zero, because the tree has no state owner or a parent view passing down a value.

This is the main measurement of the lesson: a preview gives the view values not from a real source but from its own sample model; when such a value changes, the view obeys the dependency rule just as it does in the running app. For this reason, seeing in a preview which properties a view reads is a cheap way to see in which changes that view will be recomputed.

**IP21.** This measurement treats the sample model as an object with two properties. The collections and nested objects of a real model were not measured.

**IP22.** This measurement represents the preview overriding the color scheme with the change of a single environment key. The preview overriding several environment values at the same time was not measured.

## Iteration and Feedback

Iteration is changing the code and seeing the result again. A preview shortens this loop, because it builds the view only with its own sample data. Two separate things must not be confused here. Changing the source of the view may cause the preview to be rebuilt. Changing a property of the sample model causes a running view to be recomputed. The first is a code change and is not the subject of the model; the second is of the kind we have measured throughout this course.

The practical consequence is this. If a view is recomputed in a preview more often than expected, we look for the reason with one of these four questions: is the value the view's local state, a value passed by the parent, a property of a model, or a value from the environment? The preview does not answer this question, but it provides a fast environment in which to try the answer.

**IP23.** This measurement does not represent the duration of the iteration loop in any way. How fast the preview refreshes was not measured.

**IP24.** This measurement does not count the rebuilding of a preview from source as a change. The effect of source edits on the tree was not measured.

## The Bounding Measurement

Opening a preview is not a change; it is a new starting state. Every preview builds the tree from scratch. The model builds this as a new `ViewTree` and does not count a recomputation. In the block below we build two previews for the same views. The opening of one leaves no record. The change is made in only one of them and does not affect the other.

```swift
// main.swift — a preview is a new starting state
let storage: [String: Storage] = [
    "displayName": .model(object: "ProfileStore"),
]

let nodes = [
    ViewNode("ProfileScreen", parent: nil, reads: []),
    ViewNode("NameLabel", parent: "ProfileScreen", reads: ["displayName"]),
]

let first = ViewTree(nodes: nodes, storage: storage)
let second = ViewTree(nodes: nodes, storage: storage)

let firstRun = first.change("sample-name-changed", key: "displayName")
print(firstRun.count)
print(second.nodes.count)
```

```
sample-name-changed: displayName changed | recomputed NameLabel observed | skipped ProfileScreen
1
2
```

That building leaves no record is read from the code itself: the `ViewTree` initializer only stores the nodes and the storage table, and a recomputation is produced only by a `change` call. The first line says that the change recomputed only `NameLabel` in the first tree and skipped `ProfileScreen`; the second line is `1`, the number of those recomputations. The third line shows that the second tree is built from scratch, with two nodes and unchanged: a change in one preview is not reflected in the other.

## Course Wrap-Up

Over the course, twenty-one lessons each measured the reason for a recomputation. The table below shows the change each lesson measured, the lesson's trigger, and what its bounding measurement could not measure.

| Lesson | Change | Trigger | Bounding measurement |
|---|---|---|---|
| Declarative Syntax | six changes | all four (2/2/2/2) | a value that is not stored produces no trigger |
| Views and Modifiers | font changed | environment | a child that sets its own font does not read the environment |
| Stack Layouts | spacing changed | parent | a sibling layer cannot read the value |
| The Layout System | became compact | parent | the proposed width is out of scope |
| Grids and Geometry | class changed | environment | the reader's size is out of scope |
| Lists and Forms | title changed | observed | a single row leaves the other rows skipped |
| Visual Components | color scheme changed | environment | a static image reads no value |
| View Builders | title changed | parent | the closure is not a node in the tree |
| Local State | tapped | local | the sibling's state cannot be read |
| Bindings | child wrote | parent | the rule does not read the writer |
| Observable Objects | book added | observed | object-level notification is broader |
| Environment Values | mode changed | environment | an override is not measured with a single record |
| The Navigation Stack and Path | screen pushed | local | the pushed destination is a new node |
| Presentation and Dismissal | sheet opened | local | the dismiss action is not a value |
| Tabs and Toolbars | switched to archive | local | an unselected tab is skipped |
| Gestures | drag ended | local | drag velocity is not stored |
| Implicit and Explicit Animations | panel opened | local | the animation difference is not measured |
| Transitions | message deleted | observed | the departing view is not in the tree |
| Accessibility | text enlarged | environment | a fixed label reads no value |
| Localization | language changed | environment | formatting is a pure function |
| Previews and Iteration | sample name changed | observed | a preview is a new starting state |

The trigger distribution of the twenty lessons other than the opening lesson is as follows: local 6, parent 4, observed 4, environment 6. The core in the opening lesson additionally gave a 2/2/2/2 distribution. The two are not the same thing: the core counts eight recomputations, while the distribution counts the triggers of the twenty lessons other than the opening lesson.

## Summary

- A preview gives a view a sample model and, when needed, overridden environment values; what is measured is these values.
- When a sample model property changes, the view that reads it is recomputed through the observed trigger.
- When an environment value overridden by the preview changes, the view that reads it is recomputed through the environment trigger.
- Opening a preview is a new starting state, not a change; the tree is built from scratch and no recomputation is counted.
- Each of the twenty-one lessons measured one trigger; the bounding measurements showed what the model cannot measure.
- This lesson's trigger: **observed**.

## Next Step

This course ended with measuring why a declarative interface is recomputed. The next course will change the subject area: we will grasp a screen written with an imperative interface framework and build a bridge between the two. The question and the four triggers we learned in this course will be the language we rely on when comparing the two sides of that bridge.
