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
- "Updated" says nothing.
- An announcement doesn't replace a missing label, value or focus target.
.announcementisn't for a new screen.- No timer value every second.
- A haptic doesn't replace spoken state.
WCAG mapping
4.1.3 Status Messages: status is available without moving focus.2.2.1 Timing Adjustable: a temporary action stays long enough to use.3.2.2 On Input: typing doesn't cause disruptive changes.