// // AccessibilityCustomRotorable.swift // VDS // // Created by Matt Bruce on 8/24/23. // import Foundation import UIKit public protocol CustomRotorable: UIViewController { var customRotors: [CustomRotorType] { get set } } extension CustomRotorable { /// Adds CustomRotor to the accessibilityCustomRotors array. /// - Parameters: /// - name: Name that will show up in the Rotor picker. /// - trait: Any UIView that has this traits will be added to this Name. internal func addCustomRotor(with name: String, for trait: UIAccessibilityTraits) { //filter out old rotors with same name accessibilityCustomRotors = (accessibilityCustomRotors ?? []).filter { $0.name != name } //create new rotor let newRotor = AccessibilityCustomRotor(with: name, for: trait, rootView: self.view) //append rotor accessibilityCustomRotors?.append(newRotor) } /// Loads all of the custom rotors for the screen. public func loadCustomRotors() { customRotors.forEach { addCustomRotor(with: $0.name, for: $0.trait) } } } private class AccessibilityCustomRotor: UIAccessibilityCustomRotor { var views: [UIView]? var trait: UIAccessibilityTraits weak var rootView: UIView? init (with name: String, for trait: UIAccessibilityTraits, rootView: UIView){ self.rootView = rootView self.trait = trait super.init(name: name, itemSearch: { _ in return nil }) self.updateSearch() } func updateSearch() { itemSearchBlock = { [weak self] predicate in guard let self, let rootView = self.rootView else { return nil } //create the view accessibleElements cache if it doesn't exist if self.views == nil { self.views = rootView.accessibleElements(with: trait) } guard let views = self.views, !views.isEmpty else { return nil } let currentIndex = views.firstIndex(where: { $0 === predicate.currentItem.targetElement }) let count = views.count //find the nextIndex let nextIndex: Int switch predicate.searchDirection { case .next: if let currentIndex, currentIndex != count - 1{ //go forwards nextIndex = currentIndex + 1 } else { //get the first nextIndex = 0 } case .previous: if let currentIndex, currentIndex != 0 { //go backwards nextIndex = currentIndex - 1 } else { //get the last nextIndex = count - 1 } @unknown default: //get the first nextIndex = 0 } return UIAccessibilityCustomRotorItemResult(targetElement: views[nextIndex], targetRange: nil) } } } public extension UIView { /// Gets all of the Views that has the matching accessibilityTrait. /// - Parameter trait: This is the trailt for the accessibilityTrait property of a view. /// - Returns: An array of RotorItemResult func accessibleElements(with trait: UIAccessibilityTraits) -> [UIView] { var elements: [UIView] = [] //add your self if you meet the requirements if isAccessibilityElement, accessibilityTraits.contains(trait) { elements.append(self) } //loop through your subviews subviews.forEach { elements.append(contentsOf: $0.accessibleElements(with: trait)) } return elements } } public struct CustomRotorType { public var name: String public var trait: UIAccessibilityTraits public init(name: String, trait: UIAccessibilityTraits) { self.name = name self.trait = trait } }