---
title: Accessibility
source: 'https://academia.sh/en/courses/swiftui/accessibility'
course: 'UI Development with SwiftUI'
language: en
updated: '2026-10-21T05:03:13+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that Dynamic Type size recomputes only the view that reads it, through the environment trigger'
    - 'Measures that the Reduce Motion preference is read as an environment value'
    - 'Shows that a label given as a fixed string reads no value'
---

# Accessibility

Measuring that Dynamic Type size and the Reduce Motion preference cause recomputation through the environment trigger, and that a label text supplied by the parent does so through the parent trigger.

In the previous lesson we saw that an item deleted from the model recomputes the list view through the observed trigger. In this lesson the change does not come from inside the app; it comes from the user's system settings. The question is again the same: **what triggered this recomputation?** The trigger examined in this lesson is **environment**.

Accessibility means that an interface can be used by users with different needs. The framework carries this information in three ways. The first is the label: `.accessibilityLabel(_:)` says what a view is introduced as to assistive technology. The second is the accessibility trait: `.accessibilityAddTraits(_:)` declares which role a view has, such as button, header or image. The third is the system preference: the user may have enlarged the text size, reduced motion or changed the color scheme, and these preferences reach views as environment values. Dynamic Type size is read with `@Environment(\.dynamicTypeSize)`, and the Reduce Motion preference with `@Environment(\.accessibilityReduceMotion)`. The first two paths are the view's own declaration; the third is a value that comes from outside the view. The real difference in terms of the trigger lies here.

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

## Three Paths, Three Reads

The fragment below has an article screen. The header reads the text size. The summary card uses a text given by the screen as its label, and opens either with or without an animation depending on the Reduce Motion preference. The label of the close button is a fixed string.

```swift
import SwiftUI

struct ArticleScreen: View {
    @State private var summary = "Summary"

    var body: some View {
        VStack {
            ArticleHeader()
            SummaryCard(label: summary)
            CloseButton()
        }
    }
}

struct ArticleHeader: View {
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        Text("Article")
            .font(typeSize.isAccessibilitySize ? .title : .headline)
            .accessibilityAddTraits(.isHeader)
    }
}

struct SummaryCard: View {
    let label: String
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var isOpen = false

    var body: some View {
        Text("Summary")
            .accessibilityLabel(label)
            .onTapGesture {
                if reduceMotion {
                    isOpen.toggle()
                } else {
                    withAnimation {
                        isOpen.toggle()
                    }
                }
            }
    }
}

struct CloseButton: View {
    var body: some View {
        Button("Close") {}
            .accessibilityLabel("Close the article")
    }
}
```

This fragment does not compile and produces no output; it stands as source only. The reading layout is as follows. `ArticleScreen` owns `summary` and passes it to the card's label in its body; that is, it reads the value. `ArticleHeader` reads the `dynamicTypeSize` environment value. `SummaryCard` reads `summary` as the label parameter and the `accessibilityReduceMotion` environment value in its tap handler. `CloseButton` reads no value: its label is a string constant. The model builds the framework's dependency-tracking rule; it is not the framework itself.

We write three changes: the user enlarges the text size, turns on the Reduce Motion preference, and the screen's summary text changes. Changing an environment value is, in this lesson, a system setting, not a modifier.

```swift
// main.swift — text size, reduce motion and label text
let storage: [String: Storage] = [
    "summary": .state(owner: "ArticleScreen"),
    "dynamicTypeSize": .environment,
    "accessibilityReduceMotion": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ArticleScreen", parent: nil, reads: ["summary"]),
    ViewNode("ArticleHeader", parent: "ArticleScreen", reads: ["dynamicTypeSize"]),
    ViewNode("SummaryCard", parent: "ArticleScreen", reads: ["summary", "accessibilityReduceMotion"]),
    ViewNode("CloseButton", parent: "ArticleScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("text-enlarged", key: "dynamicTypeSize"),
    screen.change("motion-reduced", key: "accessibilityReduceMotion"),
    screen.change("summary-changed", key: "summary"),
].flatMap { $0 }

countCauses(recomputes)
```

```
text-enlarged: dynamicTypeSize changed | recomputed ArticleHeader environment | skipped ArticleScreen,SummaryCard,CloseButton
motion-reduced: accessibilityReduceMotion changed | recomputed SummaryCard environment | skipped ArticleScreen,ArticleHeader,CloseButton
summary-changed: summary changed | recomputed ArticleScreen local, SummaryCard parent | skipped ArticleHeader,CloseButton
local: 1
parent: 1
observed: 0
environment: 2
```

When the text size changes, only the header is recomputed, and the trigger is environment: the value is neither the header's state nor its parent's; it is an environment value supplied by the system. The Reduce Motion preference recomputes only the card in the same way. In the third line the label text changes: the screen that owns it is recomputed through the local trigger, and the card that reads the text as a parameter through the parent trigger. The card also reading an environment value does not affect this line, because the changed key determines the trigger.

One property of the environment trigger is that no view makes the change. For the text size there is no assignment, binding or callback; the value changes outside the view, and the view that reads it is recomputed. For this reason reading an environment value is a dependency declaration: the view's body becomes bound to that value.

The measure of an environment value stays the same throughout the course: whoever reads the value is the only one recomputed. In the accessibility context, though, this rule has one more consequence. If a view at the top of the tree reads a wide-reaching value such as the text size, not everything beneath that view is affected; only the reading view is recomputed. The views beneath are recomputed only if they also read the same value or receive a parameter that changes from the parent. That is, in this model, adding accessibility support does not make the whole screen recomputed; how the framework scales text internally was not measured. How this rule scales as the number of views grows was not measured in this lesson.

**IP13.** This measurement holds only for a tree in which `ArticleHeader` reads `dynamicTypeSize` and `SummaryCard` reads `accessibilityReduceMotion`. A single view that reads both values was not measured.

**IP14.** This measurement does not say which steps the text size is divided into, how much space the header takes on the screen, or the line count. These three things were not measured.

## What a Label Reads

What a label reads depends on how you write it. The card's label came from the screen's state; when the screen changed the text, the card was recomputed. The label of the close button is a constant and is not bound to any value. It is important that a fixed label is read properly by assistive technology, but because the label reads no environment or state value, it does not trigger a recomputation either.

This distinction keeps accessibility from being counted as extra work. Labels and traits are mostly fixed texts or texts from the model, and the measurement shows that they bring no extra recomputation: only if the label's value comes from a state, the view is recomputed when that state changes. System preferences are separate. A screen reacting to the text size or the motion preference is limited to the views that read that environment value, and the rest of the views are unaffected.

**IP15.** This measurement rests on a pattern in which the label is given as a string. The case where the label comes from a localized key is the subject of the next lesson and was not measured here.

**IP16.** This measurement does not cover how assistive technology reads the label, the order, or how the traits are read out. These were not measured.

## The Bounding Measurement

We show in a separate block that a label given as a fixed string reads no value. The tree has only the header and the close button. The text size changes; the header reads this value, the button does not. Then the Reduce Motion preference changes; nobody in this tree reads it.

```swift
// main.swift — a fixed label reads no value
let storage: [String: Storage] = [
    "dynamicTypeSize": .environment,
    "accessibilityReduceMotion": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ArticleHeader", parent: nil, reads: ["dynamicTypeSize"]),
    ViewNode("CloseButton", parent: nil, reads: []),
], storage: storage)

let recomputes = [
    screen.change("text-enlarged", key: "dynamicTypeSize"),
    screen.change("motion-reduced", key: "accessibilityReduceMotion"),
].flatMap { $0 }

print(recomputes.count)
```

```
text-enlarged: dynamicTypeSize changed | recomputed ArticleHeader environment | skipped CloseButton
motion-reduced: accessibilityReduceMotion changed | recomputed none | skipped ArticleHeader,CloseButton
1
```

The close button is in the `skipped` list on both lines: because its label is fixed, it is bound neither to the text size nor to any other environment value. On the second line there is no reading view, so the recomputed list says `none`. The `1` at the end shows that only one of the two changes produced a recomputation. This shows that a change of system preference does not always affect a view. How the button grows with the text size, how the framework scales text, and how the layout adapts to it were not measured; the model counts only which view reads the value.

## Summary

- Accessibility information is carried in three paths: label, trait and system preference.
- Dynamic Type size and the Reduce Motion preference are environment values; only the view that reads them is recomputed through the environment trigger.
- If the label comes from the screen's state, the view that reads it is recomputed through the parent trigger when that state changes.
- A label given as a fixed string reads no value and causes no recomputation.
- This lesson's trigger: **environment**.

## Next Step

One of the system preferences has not been covered yet: the user's language and region. In the next lesson we will measure which views the locale and the layout direction recompute as environment values, and through which channel the property of a model that formats a date arrives.
