The accessibilityRotor view modifier (SwiftUI Release 3, iOS 15+) builds a custom VoiceOver
rotor: a named subset of elements the user flicks through directly. Reach for it when a long
list has a category worth jumping between, such as only the negative trends.
Declaring a Rotor
The modifier takes a label (String, LocalizedStringKey, or Text) and an
AccessibilityRotorContentBuilder closure. ForEach and Group work inside it, and each
AccessibilityRotorEntry binds to a view by ID. SwiftUI matches the entry IDs against the IDs
in the list's own ForEach.
struct TrendsView: View {
let trends: [Trend]
var body: some View {
List {
ForEach(trends, id: \.id) { trend in
TrendView(trend: trend)
}
}
.accessibilityRotor("Negative trends") {
ForEach(trends, id: \.id) { trend in
if !trend.isPositive {
AccessibilityRotorEntry(trend.message, id: trend.id)
}
}
}
}
}
Make the row itself one element first, for example accessibilityElement(children: .combine)
on the row's HStack, so a rotor stop lands on a sensible unit.
Explicit Binding with a Namespace
When ID matching is not enough, for example when only part of a row belongs in the rotor, bind
explicitly: tag the view with accessibilityRotorEntry(id:in:) and a @Namespace, and pass the
same namespace to the entry.
@Namespace private var customRotorNamespace
// on the target view
TrendView(trend: trend)
.accessibilityRotorEntry(id: trend.id, in: customRotorNamespace)
// in the rotor
AccessibilityRotorEntry(trend.message, trend.id, in: customRotorNamespace)
Preparing Entries and Text Ranges
An entry accepts a closure that runs when the user navigates to it. Use it with
ScrollViewReader to scroll an off-screen row into view before VoiceOver lands on it.
AccessibilityRotorEntry(trend.message, trend.id, in: customRotorNamespace) {
scrollView.scrollTo(trend.id)
}
Rotors also work inside text: accessibilityRotor(_:textRanges:) on a TextEditor lets
VoiceOver jump between marked ranges, such as emails or links.
TextEditor(text: $content.text)
.accessibilityRotor("Emails", textRanges: content.emailRanges)
.accessibilityRotor("Links", textRanges: content.linkRanges)