Announcement Spam
Status: ready
Symptom
The app announces every small change: each keystroke in search, each badge update, each timer tick, each loading step. VoiceOver speech is interrupted before the user finishes hearing anything.
Where it shows up
Search, cart badges, maps, feeds, delivery tracking, media controls, live validation, order status screens.
Why it blocks the task
An announcement interrupts whatever VoiceOver is saying. Reading a product card gets cut off by "Updated". Typing a card number gets cut off by "Invalid" after every digit. A 1-second timer competes with everything else. When everything is announced, the one announcement that matters is lost in the stream.
This is usually the overcorrection of Silent Error: the team learned that silent changes are bad and started announcing all of them.
Root cause
No rule separates a task result from a cosmetic update.
- Every state change posts an announcement.
- Stable state (count, progress) isn't in
accessibilityValue, so announcing is the only way to expose it. - Frequent updates aren't debounced.
- Validation runs on every keystroke and announces.
- Nobody decided what is allowed to interrupt the user.
Before
func searchTextDidChange(_ text: String) {
reloadResults()
UIAccessibility.post(notification: .announcement, argument: "Results updated")
}
What the user hears while typing "jasmine":
j
Results updated
ja
Results updated
jas
Results updated
Fix
Verdict: announce only a task result, once. Keep changing state in accessibilityValue where the user can read it on demand.
Why: the value is always there when the user asks. The announcement interrupts. Only a result the user is waiting for justifies an interruption.
| Change | What to do |
|---|---|
| Search results while typing | Announce the count once, after typing stops (about 600 ms) |
| Added to cart | One announcement: "Added to cart. 3 items." |
| Cart badge count | accessibilityValue on the cart button |
| Upload progress | accessibilityValue + .updatesFrequently. Announce completion only |
| Countdown timer | accessibilityValue. Announce at 2 minutes and at 30 seconds, not every second |
| Field validation | On submit, not per keystroke |
SwiftUI, search count after typing stops:
.task(id: query) {
do { try await Task.sleep(for: .milliseconds(600)) } catch { return }
AccessibilityNotification.Announcement(
results.isEmpty ? "No results" : "\(results.count) results"
).post()
}
Each new keystroke cancels the previous task, so only the last one announces.
UIKit, progress:
progressView.accessibilityLabel = "Uploading receipt"
progressView.accessibilityValue = "60 percent"
progressView.accessibilityTraits.insert(.updatesFrequently)
// on completion only
UIAccessibility.post(notification: .announcement, argument: "Receipt uploaded")
What we don't do
- We don't announce on every keystroke.
- We don't announce a value that's already in
accessibilityValueon the focused element. VoiceOver reads it anyway. - We don't use an announcement for an error that needs a fix. That needs focus (Focus Recovery).
Check
Profile: VoiceOver
Task: type "jasmine" in search
Expected: nothing announced while typing
one announcement with the count after typing stops
Profile: VoiceOver
Task: add an item to the cart, then move to the cart button
Expected: one confirmation
the cart button reads the count as its value
Profile: VoiceOver
Task: stay on the order status screen for 60 seconds
Expected: no speech unless the order stage changes
the timer value is read when focused
WCAG mapping
4.1.3 Status Messages: status changes are available without moving focus.2.2.2 Pause, Stop, Hide: auto-updating content doesn't take over.3.2.2 On Input: typing doesn't cause unexpected changes.
App Store and product risk
Not a rejection. It makes the flow tiring enough that VoiceOver users give up. A team that claims VoiceOver support needs task completion evidence, not "we added announcements".
Related patterns
- Meaningful Announcement
- State as Value
- Loading State Contract
- Time Extension
- Testing Evidence Record