Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

Code shown on screen · 9 snippets

mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)] swift · at 4:04 ↗
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
map rotor 1 swift · at 4:31 ↗
// Custom map rotors

func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
    UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in

        return UIAccessibilityCustomRotorItemResult(    )
    }
}
map rotor 2 swift · at 4:56 ↗
// 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 swift · at 5:04 ↗
// 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 swift · at 5:17 ↗
// 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 swift · at 8:07 ↗
// 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 swift · at 8:20 ↗
// 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 swift · at 8:37 ↗
// 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) swift · at 9:06 ↗
// 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