SwiftUI views are accessible by default, but a composite control like a five-star rating reads as five meaningless "star fill" buttons. Custom accessibility actions turn it into one adjustable element, and predefined gestures like magic tap map to the view's main action.
Adjustable Elements
Collapse the container into one element with accessibilityElement(), give it a label and a
value, and handle accessibilityAdjustableAction, which also adds the adjustable trait.
VoiceOver then announces "rating, 3, adjustable" and swipe up or down changes the value.
struct RatingView: View {
@Binding var rating: Int
var body: some View {
HStack {
ForEach(1..<6) { index in
Button(action: { rating = index }) {
Image(systemName: index <= rating ? "star.fill" : "star")
}
}
}
.accessibilityElement()
.accessibilityLabel(Text("rating"))
.accessibilityValue(Text(String(rating)))
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
guard rating < 5 else { break }
rating += 1
case .decrement:
guard rating > 1 else { break }
rating -= 1
@unknown default:
break
}
}
}
}
A stack is normally a transparent accessibility container that exposes its children;
accessibilityElement() replaces that with one element and hides the children.
Predefined and Named Actions
Magic tap (two-finger double tap) should trigger the view's main action; escape (two-finger z-scrub) should back out or dismiss.
PlayerContent()
.accessibilityAction(.magicTap) {
viewModel.isPlaying ? viewModel.pause() : viewModel.play()
}
.accessibilityAction(.escape) {
viewModel.pause()
presentation.wrappedValue.dismiss()
}
Named actions appear in the VoiceOver rotor menu for the element, giving swipe-reachable alternatives to small buttons.
.accessibilityAction(named: Text("skip")) { viewModel.skip() }
.accessibilityAction(named: Text("repeat")) { viewModel.repeat() }