How do custom actions, adjustable controls, rotors and announcements make complex UI usable with VoiceOver?
Predict first
Two custom actions are attached to a row. What names does the element expose?
import UIKit
@MainActor func run() {
let row = UIView()
row.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Archive") { _ in true },
UIAccessibilityCustomAction(name: "Delete") { _ in true },
]
print(row.accessibilityCustomActions?.map(\.name) ?? [])
}
MainActor.assumeIsolated { run() }
- ["Archive", "Delete"]
- []
- It crashes outside VoiceOver
Predict first
Do announcement, layout-changed and screen-changed notifications compile for UIKit and SwiftUI?
import UIKit
import SwiftUI
@MainActor func announce() {
UIAccessibility.post(notification: .announcement, argument: "Upload finished")
UIAccessibility.post(notification: .layoutChanged, argument: nil)
UIAccessibility.post(notification: .screenChanged, argument: nil)
AccessibilityNotification.Announcement("3 new messages").post()
}
- Yes
- No, only UIKit has them
Mental model. If a shop’s best offers are on a high shelf that only tall people can reach, you put the same offers on a menu card too. Custom actions are the menu card for gestures.
A visual interface hides actions in gestures: swipe a row to delete, long-press to reveal a menu, drag to reorder. A VoiceOver user cannot perform those gestures, so expose the same actions another way. Custom actions add named operations to an element (VoiceOver offers them with a swipe up or down on the element and says “actions available”): accessibilityCustomActions in UIKit, .accessibilityAction(named:) in SwiftUI. They replace swipe actions and long-press menus without cluttering the row with several tappable children (the check below builds two). For a value the user changes by sliding, make the element adjustable (the adjustable trait with accessibilityIncrement() and accessibilityDecrement(), or .accessibilityAdjustableAction) so a swipe up or down changes it and VoiceOver reads the new value. A custom rotor (UIAccessibilityCustomRotor, .accessibilityRotor) lets users jump between meaningful items in a long screen, such as errors in a form or unread messages, instead of swiping through everything.
VoiceOver also needs to be told about changes the user did not trigger. Post a notification: .announcement speaks a message (“Upload finished”, “3 new messages”) without moving focus, .layoutChanged tells VoiceOver part of the screen changed and optionally moves focus to an element, and .screenChanged says the whole screen changed (after a navigation without a standard push). In SwiftUI the equivalent is AccessibilityNotification.Announcement(…).post() and @AccessibilityFocusState to move focus, for example to an error message after a failed submit. Do not over-announce, because every announcement interrupts; move focus when a new element needs attention (an alert, an inline error) and announce quietly otherwise. Keep drag and drop, charts and canvases usable too: give a chart a summary label and an accessible data representation (audio graphs, a table), and offer a non-gesture alternative to any path-based gesture.
Custom actions are plain data on the element; VoiceOver presents them when it is running.
Both frameworks provide ways to tell assistive technology about changes.
Common traps
- Hiding the only way to delete or reorder behind a swipe gesture. Add the action as a custom action too.
- Announcing every small change. Constant interruptions are worse than silence; announce important results and move focus for new content.
They might follow up with
- How does a slider-like custom control work with VoiceOver? Mark it adjustable, implement increment and decrement, and expose its value.
- When do you use
.layoutChangedversus.screenChanged? The first for part of the screen changing, the second for a whole new screen.
Related questions
- 24.1.1 How does VoiceOver read your UI: accessibility elements, labels, values, hints and traits, and how do you fix…
- 24.1.3 How do Voice Control, Switch Control and keyboard access use your UI, and what do accessibility input labels…
- 24.5.1 How do you test accessibility: the Accessibility Inspector, automated audits in XCUITest, identifiers versus…
- 7.8.1 How do you support hardware keyboards, shortcuts, menus and the pointer on iPad?
← How does VoiceOver read your UI: accessibility elements, labels… How do Voice Control, Switch Control and keyboard access use your UI… →