2020 Accessibility & Inclusion
WWDC20 · 11 min · Accessibility & Inclusion
VoiceOver efficiency with custom rotors
Discover how you can integrate custom rotors and help people who use VoiceOver navigate complex situations within your app. Learn how custom rotors can help people explore even the most intricate interfaces, explore how to implement a custom rotor, and find out how rotors can improve navigation for someone who relies on VoiceOver. To get the most out of this session, you should be familiar with general accessibility principles and VoiceOver accessibility APIs on iOS and iPadOS. For an overview, watch “Making Apps More Accessible with Custom Actions.”
Watch at developer.apple.com ↗Code shown on screen · 9 snippets
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)] map rotor 1
// Custom map rotors
func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in
return UIAccessibilityCustomRotorItemResult( )
}
} map rotor 2
// Custom map rotors
func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in
let currentElement = predicate.currentItem.targetElement as? MKAnnotationView
let annotations = self.annotationViews(for: poiType)
let currentIndex = annotations.firstIndex { $0 == currentElement }
return UIAccessibilityCustomRotorItemResult( )
}
} map rotor 3
// Custom map rotors
func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in
let currentElement = predicate.currentItem.targetElement as? MKAnnotationView
let annotations = self.annotationViews(for: poiType)
let currentIndex = annotations.firstIndex { $0 == currentElement }
let targetIndex: Int
switch predicate.searchDirection {
case .previous:
targetIndex = (currentIndex ?? 1) - 1
case .next:
targetIndex = (currentIndex ?? -1) + 1
}
return UIAccessibilityCustomRotorItemResult( )
}
} Maps rotor 4
// Custom map rotors
func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in
let currentElement = predicate.currentItem.targetElement as? MKAnnotationView
let annotations = self.annotationViews(for: poiType)
let currentIndex = annotations.firstIndex { $0 == currentElement }
let targetIndex: Int
switch predicate.searchDirection {
case .previous:
targetIndex = (currentIndex ?? 1) - 1
case .next:
targetIndex = (currentIndex ?? -1) + 1
}
guard 0..<annotations.count ~= targetIndex else { return nil } // Reached boundary
return UIAccessibilityCustomRotorItemResult(targetElement: annotations[targetIndex],
targetRange: nil)
}
} Text rotor 1
// Custom text rotor
func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in
var targetRange: UITextRange? // Goal: find the range of following `attribute`
let beginningRange =
guard let currentRange = else { return nil }
switch predicate.searchDirection { }
return UIAccessibilityCustomRotorItemResult(targetElement: self,
targetRange: targetRange)
}
} Text rotor 2
// Custom text rotor
func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in
var targetRange: UITextRange? // Goal: find the range of following `attribute`
let beginningRange = self.textRange(from: self.beginningOfDocument,
to: self.beginningOfDocument)
guard let currentRange = predicate.currentItem.targetRange ?? beginningRange else {
return nil
}
let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions
switch predicate.searchDirection { }
return UIAccessibilityCustomRotorItemResult(targetElement: self,
targetRange: targetRange)
}
} Text rotor 3
// Custom text rotor
func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in
var targetRange: UITextRange? // Goal: find the range of following `attribute`
let beginningRange =
guard let currentRange = else { return nil }
let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions
switch predicate.searchDirection {
case .previous:
searchRange = self.rangeOfAttributedTextBefore(currentRange)
searchOptions = [.reverse]
case .next:
searchRange = self.rangeOfAttributedTextAfter(currentRange)
searchOptions = []
}
return UIAccessibilityCustomRotorItemResult(targetElement: self,
targetRange: targetRange)
}
} Text rotor 4 (end)
// Custom text rotor
func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor {
UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in
var targetRange: UITextRange? // Goal: find the range of following `attribute`
let beginningRange =
guard let currentRange = else { return nil }
let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions
switch predicate.searchDirection { }
self.attributedText.enumerateAttribute(
attribute, in: searchRange, options: searchOptions) { value, range, stop in
guard value != nil else { return }
targetRange = self.textRange(from: range)
stop.pointee = true
}
return UIAccessibilityCustomRotorItemResult(targetElement: self,
targetRange: targetRange)
}
} Resources
Related sessions
-
26 min -
28 min -
9 min