From ab16fb4642733e330a374bfb7a4bf52edc59a831 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 24 Aug 2023 14:19:46 -0500 Subject: [PATCH] added rotor Signed-off-by: Matt Bruce --- VDSSample.xcodeproj/project.pbxproj | 4 + VDSSample/Protocols/CustomRotorable.swift | 107 ++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 VDSSample/Protocols/CustomRotorable.swift diff --git a/VDSSample.xcodeproj/project.pbxproj b/VDSSample.xcodeproj/project.pbxproj index 6bb1d06..42d240b 100644 --- a/VDSSample.xcodeproj/project.pbxproj +++ b/VDSSample.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ EA3C3BB528996775000CA526 /* StoryboardInitable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB128996775000CA526 /* StoryboardInitable.swift */; }; EA3C3BB628996775000CA526 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB228996775000CA526 /* MenuViewController.swift */; }; EA3C3BB728996775000CA526 /* ToggleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB328996775000CA526 /* ToggleViewController.swift */; }; + EA471F402A97BEAA00CE9E58 /* CustomRotorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA471F3F2A97BEAA00CE9E58 /* CustomRotorable.swift */; }; EA4DB30428DCD25B00103EE3 /* BadgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB30328DCD25B00103EE3 /* BadgeViewController.swift */; }; EA596ABA2A16B2ED00300C4B /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA596AB92A16B2ED00300C4B /* TabsViewController.swift */; }; EA5E3050294D11540082B959 /* TileContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E304F294D11540082B959 /* TileContainerViewController.swift */; }; @@ -144,6 +145,7 @@ EA3C3BB128996775000CA526 /* StoryboardInitable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardInitable.swift; sourceTree = ""; }; EA3C3BB228996775000CA526 /* MenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; EA3C3BB328996775000CA526 /* ToggleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleViewController.swift; sourceTree = ""; }; + EA471F3F2A97BEAA00CE9E58 /* CustomRotorable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRotorable.swift; sourceTree = ""; }; EA4DB30328DCD25B00103EE3 /* BadgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeViewController.swift; sourceTree = ""; }; EA596AB92A16B2ED00300C4B /* TabsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsViewController.swift; sourceTree = ""; }; EA5E304F294D11540082B959 /* TileContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainerViewController.swift; sourceTree = ""; }; @@ -294,6 +296,7 @@ EAF7F07E28996A0700B287F5 /* Protocols */ = { isa = PBXGroup; children = ( + EA471F3F2A97BEAA00CE9E58 /* CustomRotorable.swift */, EA3C3BB028996775000CA526 /* PickerBase.swift */, EA3C3BB128996775000CA526 /* StoryboardInitable.swift */, ); @@ -498,6 +501,7 @@ 445BA07A29C088470036A7C5 /* NotificationViewController.swift in Sources */, EAF7F11A28A14A0E00B287F5 /* RadioButtonGroupViewController.swift in Sources */, EA89204628B66CE2006B9984 /* ScrollViewController.swift in Sources */, + EA471F402A97BEAA00CE9E58 /* CustomRotorable.swift in Sources */, EA3C3B9F289966EF000CA526 /* SceneDelegate.swift in Sources */, EA0D1C332A673FD400E5C127 /* RadioButtonItemViewController.swift in Sources */, 5FC35BE928D5235A004EBEAC /* ButtonViewController.swift in Sources */, diff --git a/VDSSample/Protocols/CustomRotorable.swift b/VDSSample/Protocols/CustomRotorable.swift new file mode 100644 index 0000000..6b303d3 --- /dev/null +++ b/VDSSample/Protocols/CustomRotorable.swift @@ -0,0 +1,107 @@ +// +// AccessibilityCustomRotorable.swift +// VDS +// +// Created by Matt Bruce on 8/24/23. +// + +import Foundation +import UIKit + +public protocol CustomRotorable: UIViewController { + var customRotorCache: [String: [UIView]] { get set } + 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 = UIAccessibilityCustomRotor(name: name) { [weak self] predicate in + guard let self else { return nil } + + //create the view accessibleElements cache if it doesn't exist + if self.customRotorCache[name] == nil { + self.customRotorCache[name] = self.view.accessibleElements(with: trait) + } + + guard let views = self.customRotorCache[name], !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) + } + + //append rotor + accessibilityCustomRotors?.append(newRotor) + } + + /// Loads all of the custom rotors for the screen. + public func loadCustomRotors() { + customRotorCache.removeAll() + UIAccessibility.post(notification: .screenChanged, argument: nil) + customRotors.forEach { addCustomRotor(with: $0.name, for: $0.trait) } + } +} + +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 + } +}