---
title: 'Visual Components'
source: 'https://academia.sh/en/courses/swiftui/visual-components'
course: 'UI Development with SwiftUI'
language: en
updated: '2026-10-21T05:03:15+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads in a source snippet how leaf components such as text, image and shape are built'
    - 'Measures that the color scheme determines the icon color as an environment value'
    - 'Shows that the parent state that supplies the fill ratio of a shape produces the parent trigger'
    - 'Measures that the loading state belongs to the image itself and produces the local trigger'
---

# Visual Components

A measurement showing that the color scheme determines the icon color as an environment value, that the fill ratio of a shape descends from the parent, that the loading state of an image stays local, and that a static image reads no value.

In the previous lesson the trigger came from a model outside the tree. In this lesson we look at the leaves of the tree: text, icon, shape and image. These components do not contain other views, but they do read values: the color of an icon, the fill of a shape and the loading state of an image each depend on a value. The question stays the same: **what triggered this recomputation?** The trigger examined in this lesson is **environment**.

```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)")
    }
}
```

The core is the same as in the opening lesson. The tree of this lesson is a meter panel: the screen that builds the panel, the panel, a shape, an icon and an avatar.

## Leaf Components

Text, icon, shape and image are the leaves of the tree. Each takes its form or content from a value. The snippet below shows all four together.

```swift
// Source snippet: text, icon, shape and image
import SwiftUI

struct MeterPanel: View {
    let fillRatio: Double

    var body: some View {
        VStack {
            Text("Fill level")
            Image(systemName: "gauge")
            Capsule()
                .fill(Color.blue)
                .frame(width: 200 * fillRatio, height: 12)
            Image("logo")
        }
    }
}
```

The width of the shape comes from the panel's `fillRatio` parameter. The icon, on the other hand, takes its foreground color from the environment. `Image("logo")` is a static image built with an asset name and reads no value.

**VS20.** This measurement handles leaf components through the values they read. The drawing cost of the components, the size of the icon set and the resolution of the asset catalog were not measured.

## Color Scheme and Fill Ratio

In the tree below the color scheme comes from the environment and is read by the icon and the avatar. The fill ratio is the state of the screen that builds the panel, `MeterScreen`; the panel takes it as a `let` parameter and passes it to the shape. The loading state of the avatar belongs to the avatar itself.

```swift
// main.swift — the color scheme is environment, the fill ratio is parent, the loading state is local
let storage: [String: Storage] = [
    "colorScheme": .environment,
    "fillRatio": .state(owner: "MeterScreen"),
    "isLoading": .state(owner: "AvatarImage"),
]

let screen = ViewTree(nodes: [
    ViewNode("MeterScreen", parent: nil, reads: ["fillRatio"]),
    ViewNode("MeterPanel", parent: "MeterScreen", reads: ["fillRatio"]),
    ViewNode("MeterShape", parent: "MeterPanel", reads: ["fillRatio"]),
    ViewNode("MeterIcon", parent: "MeterPanel", reads: ["colorScheme"]),
    ViewNode("AvatarImage", parent: "MeterPanel", reads: ["isLoading", "colorScheme"]),
], storage: storage)

let recomputes = [
    screen.change("color-scheme-changed", key: "colorScheme"),
    screen.change("fill-changed", key: "fillRatio"),
    screen.change("loading-finished", key: "isLoading"),
].flatMap { $0 }

countCauses(recomputes)
```

```
color-scheme-changed: colorScheme changed | recomputed MeterIcon environment, AvatarImage environment | skipped MeterScreen,MeterPanel,MeterShape
fill-changed: fillRatio changed | recomputed MeterScreen local, MeterPanel parent, MeterShape parent | skipped MeterIcon,AvatarImage
loading-finished: isLoading changed | recomputed AvatarImage local | skipped MeterScreen,MeterPanel,MeterShape,MeterIcon
local: 2
parent: 2
observed: 0
environment: 2
```

In the `color-scheme-changed` line the icon and the avatar were recomputed with the environment trigger; the screen, the panel and the shape were skipped. In the `fill-changed` line the screen, which owns the value, is local, and the panel and the shape were recomputed with the parent trigger. In the `loading-finished` line only the avatar was recomputed, with the local trigger: the avatar keeps the loading state within itself.

Its counterpart in source is a view that loads a remote image. While loading continues a placeholder is shown, and when it finishes the image itself is shown; when the state changes only this view is recomputed.

```swift
// Source snippet: a placeholder is shown while a remote image loads
import SwiftUI

struct AvatarImage: View {
    let url: URL?

    var body: some View {
        AsyncImage(url: url) { image in
            image.resizable()
        } placeholder: {
            ProgressView()
        }
    }
}
```

**VS21.** This measurement covers `colorScheme`, `fillRatio` and `isLoading`. Whether the loading state really is the view's own state, that is, where the framework stores this state, was not measured; in the model the avatar was counted as its owner. The screen added as the owner of the fill ratio is also a model assumption; the source snippet shows only the panel's parameter.

## The Bounding Measurement

A static image reads no value. That is why no change makes it recompute. In the tree below the logo image is a leaf with an empty read set; the panel reads its own mode, and the caption reads the panel's mode.

```swift
// main.swift — a static image reads no value
let storage: [String: Storage] = [
    "mode": .state(owner: "LogoPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("LogoPanel", parent: nil, reads: ["mode"]),
    ViewNode("StaticLogo", parent: "LogoPanel", reads: []),
    ViewNode("LogoCaption", parent: "LogoPanel", reads: ["mode"]),
], storage: storage)

let recomputes = [
    screen.change("mode-changed", key: "mode"),
    screen.change("asset-changed", key: "logoAsset"),
].flatMap { $0 }

countCauses(recomputes)
```

```
mode-changed: mode changed | recomputed LogoPanel local, LogoCaption parent | skipped StaticLogo
asset-changed: logoAsset changed | recomputed none | skipped LogoPanel,StaticLogo,LogoCaption
local: 1
parent: 1
observed: 0
environment: 0
```

In the `mode-changed` line the panel is local and the caption was recomputed with the parent trigger; the logo was skipped. In the `asset-changed` line no view was recomputed, because no view reads the key: all three views were skipped. A static image is in the tree but is not sensitive to any trigger; to be recomputed it has to read a value. The effect of its parent rebuilding it was not measured in VS22.

**VS22.** This measurement rests on the assumption that recomputing the parent does not rebuild a static child. Whether the framework reruns the bodies of the child views when the parent is recomputed was not measured.

## Summary

- Text, icon, shape and image are the leaves of the tree; each takes its form from a value.
- The color scheme is read from the environment; the icon and the avatar that read it are recomputed with the environment trigger.
- The parent state that supplies the fill ratio of a shape produces the parent trigger for the shape.
- The loading state of a remote image belongs to the image itself and produces the local trigger.
- A static image reads no value; it is not recomputed on any change.
- This lesson's trigger: **environment**.

## Next Step

In this lesson the leaves took their values from the environment, from the parent and from within themselves. In the next lesson we look at the tool that builds the tree: the view builder. Which value does content that depends on a condition, or a child view built with a closure, depend on? The trigger this time is parent.
