Meaningful Announcement

Status: ready

Intent

Announce only changes the user is waiting for. Everything else lives in a label, a value, a trait, a focus move or visible state.

Problem

Silent updates hide results. Too many announcements bury them. The answer isn't "announce more" or "announce less". It's choosing the right channel for each change.

Solution

Verdict: one channel per kind of change.

Change Channel
New screen or modal Focus moves (.screenChanged, or native presentation)
Same screen, the user has to act somewhere new Focus moves (.layoutChanged with a target)
Short result of the user's action .announcement, once
Stable state (count, selection) accessibilityValue or a trait
Frequent progress Value + .updatesFrequently. Announce completion
Anything while the user is typing Wait until typing stops

Why: an announcement interrupts whatever VoiceOver is saying. It's worth it only when the user is waiting for that result.

Good announcements: "Added to cart. 3 items.", "Receipt uploaded", "Order is on the way". Bad announcements: "Updated", "Loading", every search refresh, every timer tick.

Implementation

Result of an action:

AccessibilityNotification.Announcement("Added to cart. 3 items.").post()

Stable count, no announcement:

CartButton()
    .accessibilityValue("\(cart.count) items")

Progress:

uploadView.accessibilityLabel = "Uploading receipt"
uploadView.accessibilityValue = "60 percent"
uploadView.accessibilityTraits.insert(.updatesFrequently)

// on completion
UIAccessibility.post(notification: .announcement, argument: "Receipt uploaded")

Search results, after typing stops:

.task(id: query) {
    do { try await Task.sleep(for: .milliseconds(600)) } catch { return }
    AccessibilityNotification.Announcement("\(results.count) results").post()
}

An undo banner: the action is a real button, and the banner stays until the user moves on, not for a fixed 2 seconds.

HStack {
    Text("Message sent")
    Button("Undo") { undoSend() }
}
.accessibilityElement(children: .contain)
.onAppear {
    AccessibilityNotification.Announcement("Message sent. Undo available.").post()
}

Check

Profile:   VoiceOver
Task:      add an item to the cart, then move to the cart button
Expected:  one confirmation
           the cart reads the count as its value, without a second announcement
Profile:   VoiceOver
Task:      type "jasmine" in search
Expected:  no speech while typing
           one count after typing stops
Profile:   VoiceOver
Task:      send a message, then undo
Expected:  Undo is reachable after the announcement
           the banner doesn't disappear while the user swipes to it

Pitfalls

  1. "Updated" says nothing.
  2. An announcement doesn't replace a missing label, value or focus target.
  3. .announcement isn't for a new screen.
  4. No timer value every second.
  5. A haptic doesn't replace spoken state.

WCAG mapping

  1. 4.1.3 Status Messages: status is available without moving focus.
  2. 2.2.1 Timing Adjustable: a temporary action stays long enough to use.
  3. 3.2.2 On Input: typing doesn't cause disruptive changes.
  1. Announcement Spam
  2. Silent Error
  3. Animation Only Feedback
  4. Skeleton Noise
  5. Audio Only Task
  1. Chapter 5
  2. Chapter 11
  3. Chapter 13
  4. Chapter 17

Sources

  1. Apple Developer: AccessibilityNotification
  2. Apple Developer: AccessibilityNotification.LayoutChanged
  3. Apple Developer: Build accessible apps with SwiftUI and UIKit