From 00427f9e774ef5139132d35247fd4151a3f2e032 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Tue, 26 Sep 2023 00:15:20 +0530 Subject: [PATCH] changed to dynamic rotor --- MVMCoreUI.xcodeproj/project.pbxproj | 4 - .../Accessibility/AccessibilityHandler.swift | 126 ++++++++++++------ .../Templates/ModalMoleculeListTemplate.swift | 5 - .../MVMCoreUISession+Extension.swift | 22 --- MVMCoreUI/OtherHandlers/MVMCoreUISession.m | 1 - 5 files changed, 84 insertions(+), 74 deletions(-) delete mode 100644 MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index bb67be60..4e04dafc 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -168,7 +168,6 @@ 526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */; }; 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; - 71033C002AB60AEA0038D7A4 /* MVMCoreUISession+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */; }; 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; }; 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; }; @@ -756,7 +755,6 @@ 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnCompareChanges.swift; sourceTree = ""; }; 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethod.swift; sourceTree = ""; }; 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = ""; }; - 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUISession+Extension.swift"; sourceTree = ""; }; 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = ""; }; 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = ""; }; @@ -2279,7 +2277,6 @@ D2B18B912361E65A00A9AEDC /* CoreUIObject.swift */, D29DF27721E7A533003B2FB9 /* MVMCoreUISession.h */, D29DF27821E7A533003B2FB9 /* MVMCoreUISession.m */, - 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */, D29DF27321E79E81003B2FB9 /* MVMCoreUILoggingHandler.h */, D29DF27421E79E81003B2FB9 /* MVMCoreUILoggingHandler.m */, AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */, @@ -3096,7 +3093,6 @@ D29C559025C095210082E7D6 /* Video.swift in Sources */, D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */, AA104B1C24474A76004D2810 /* HeadersH2ButtonsModel.swift in Sources */, - 71033C002AB60AEA0038D7A4 /* MVMCoreUISession+Extension.swift in Sources */, 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */, AAE7270C24AC8B8500A3ED0E /* HeadersH2CaretLinkModel.swift in Sources */, BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */, diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index d609aecd..d25ab81e 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -191,23 +191,28 @@ open class AccessibilityHandler { // MARK: - Accessibility Handler Behaviour ///Accessibility Handler Behaviour to detect page shown and post notification to first interactive element on screen or the pre-defined focused element. -struct AccessibilityHandlerBehaviorModel: PageBehaviorModelProtocol { +open class AccessibilityHandlerBehavior: PageVisibilityBehavior { - var shouldAllowMultipleInstances = false - static var identifier = "accessibilityHandlerBehaviorModel" -} - -class AccessibilityHandlerBehavior: PageVisibilityBehavior { + enum RotorType: String, CaseIterable { + + case button = "Buttons" + + var trait: UIAccessibilityTraits { + switch self { + case .button: + return .button + } + } + } + public var anyCancellable: Set = [] private var delegateObj: MVMCoreUIDelegateObject? - private var anyCancellable: Set = [] - private var accessibilityButtons: [Any]? private var currentRotorIndex: Int = 0 required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } //MARK: - PageVisibiltyBehaviour - public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, (AccessibilityHandler.shared()?.canPostAccessbilityNotification(for: controller) ?? true), @@ -222,9 +227,9 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { ///We need to shift focus to any element mentioned in server response i.e to retain focus of the element in new page, from where action is triggered. ///https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain - func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + open func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) //To track FAB & HAB elements on UI - identifyAndPrepareForButtonRotor() + identifyAndPrepareRotors() guard let accessibilityElement = AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() else { return } if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { AccessibilityHandler.shared()?.previousAccessiblityElement = accessibilityElement @@ -234,65 +239,101 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { } //MARK: - Private Methods - private func identifyAndPrepareForButtonRotor() { + private func identifyAndPrepareRotors() { + var rotorElements: [UIAccessibilityCustomRotor] = [] let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? ViewController) - var accessibilityButtons: [Any?]? = currentViewController?.navigationItem.leftBarButtonItems ?? [] - accessibilityButtons?.append(contentsOf: currentViewController?.navigationItem.rightBarButtonItems ?? []) - if let tabs = (currentViewController as? SubNavManagerController)?.tabs { - accessibilityButtons?.append(contentsOf: tabs.subviews.filter { $0.accessibilityTraits.contains(.button) }) + for element in RotorType.allCases { + if let elements = getTraitMappedElements(template: currentViewController, type: element), + let rotor = createRotor(elements, for: element) { + rotorElements.append(rotor) + } } - if let rotorElements = getRotorButtonsBasedOn(template: currentViewController) { - accessibilityButtons?.append(contentsOf: rotorElements) - } - if let tabBarHidden = (delegateObj?.moleculeDelegate as? TabPageModelProtocol)?.tabBarHidden, !tabBarHidden { - accessibilityButtons?.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { $0.accessibilityTraits.contains(.button)}) - } - self.accessibilityButtons = accessibilityButtons?.compactMap { $0 } - currentViewController?.navigationController?.accessibilityCustomRotors = [createRotorForButtons()].compactMap { $0 } + currentViewController?.navigationController?.accessibilityCustomRotors = rotorElements } - private func getRotorButtonsBasedOn(template: ViewController?) -> [Any]? { - if let currentViewController = template as? MoleculeListTemplate, let templateModel = currentViewController.templateModel { //List templates + private func getTraitMappedElements(template: ViewController?, type: RotorType) -> [Any]? { + var accessibilityElements: [Any?]? = template?.navigationItem.leftBarButtonItems ?? [] + accessibilityElements?.append(contentsOf: template?.navigationItem.rightBarButtonItems ?? []) + if let tabs = (template as? SubNavManagerController)?.tabs { + accessibilityElements?.append(contentsOf: tabs.subviews.filter { + $0.accessibilityTraits.contains(type.trait) + }) + } + if let rotorElements = getRotorElementsFrom(template: template, type: type) { + accessibilityElements?.append(contentsOf: rotorElements) + } + if let tabBarHidden = (template as? TabPageModelProtocol)?.tabBarHidden, !tabBarHidden { + accessibilityElements?.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { + $0.accessibilityTraits.contains(type.trait) + }) + } + return accessibilityElements?.compactMap { $0 } + } + + private func getRotorElementsFrom(template: ViewController?, type: RotorType) -> [Any]? { + if let currentViewController = template as? MoleculeListTemplate, + let templateModel = currentViewController.templateModel, + let tableView = currentViewController.tableView { //List templates + var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] var currentIndexPath: IndexPath? + rotorElements = templateModel.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements, nextPartialResult: { result, model, depth in if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), let indexPath = currentViewController.getIndexPath(for: listModel) { currentIndexPath = indexPath } var result = result - if (model.accessibilityTraits?.contains(.button) ?? false), let currentIndexPath { + if (model.accessibilityTraits?.contains(type.trait) ?? false), let currentIndexPath { result.append((model, currentIndexPath)) } return result }) - let headerViewElements = currentViewController.tableView.tableHeaderView?.getMoleculeViews { (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(.button) } as? [Any] ?? [] - let footerViewElements = currentViewController.tableView.tableFooterView?.getMoleculeViews { (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(.button) } as? [Any] ?? [] - return headerViewElements + (rotorElements as [Any]) + footerViewElements + + let headerViewElements = currentViewController.tableView.tableHeaderView?.getMoleculeViews { + (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(type.trait) + } as? [Any] ?? [] + + let footerViewElements = currentViewController.tableView.tableFooterView?.getMoleculeViews { + (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(type.trait) + } as? [Any] ?? [] + + let otherInteractiveElements = currentViewController.view?.getMoleculeViews(excludedViews: [tableView, tableView.tableFooterView, tableView.tableHeaderView] + .compactMap { $0 }) { (subView: MoleculeViewProtocol) in + subView.accessibilityTraits.contains(type.trait) + } as? [Any] ?? [] + + return headerViewElements + otherInteractiveElements + (rotorElements as [Any]) + footerViewElements } else if let currentViewController = template as? MoleculeStackTemplate { //Stack templates - return currentViewController.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(.button) } + return currentViewController.view?.getMoleculeViews { + (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(type.trait) + } } return nil } - private func createRotorForButtons() -> UIAccessibilityCustomRotor? { - guard let accessibilityButtons, !accessibilityButtons.isEmpty, let tableView = (delegateObj?.moleculeListDelegate as? MoleculeListTemplate)?.tableView else { return nil } - return UIAccessibilityCustomRotor(name: "Buttons") { [weak self] predicate in - guard let self, let accessibilityButtons = self.accessibilityButtons else { return UIAccessibilityCustomRotorItemResult() } + private func createRotor(_ elements: [Any], for type: RotorType) -> UIAccessibilityCustomRotor? { + guard elements.isEmpty, let tableView = (delegateObj?.moleculeListDelegate as? MoleculeListTemplate)?.tableView else { return nil } + return UIAccessibilityCustomRotor(name: type.rawValue) { [weak self] predicate in + guard let self else { return UIAccessibilityCustomRotorItemResult() } if predicate.searchDirection == .next { self.currentRotorIndex += 1 - if self.currentRotorIndex > accessibilityButtons.count { + if self.currentRotorIndex > elements.count { self.currentRotorIndex = 1 } } else { self.currentRotorIndex -= 1 if self.currentRotorIndex <= 0 { - self.currentRotorIndex = accessibilityButtons.count + self.currentRotorIndex = elements.count } } - var rotorElement = accessibilityButtons[self.currentRotorIndex - 1] + var rotorElement = elements[self.currentRotorIndex - 1] if let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { tableView.scrollToRow(at: element.indexPath, at: .middle, animated: false) - rotorElement = tableView.cellForRow(at: element.indexPath)?.getMoleculeViews { (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(.button) }.filter { ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id }.first as Any + rotorElement = tableView.cellForRow(at: element.indexPath)?.getMoleculeViews { + (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(type.trait) + }.filter { + ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id + }.first as Any } UIAccessibility.post(notification: .layoutChanged, argument: rotorElement) return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) @@ -319,16 +360,17 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { // MARK: - Helpers extension UIView { - private func getNestedSubviews() -> [T] { + private func getNestedSubviews(excludedViews: [UIView]? = nil) -> [T] { subviews.flatMap { subView -> [T] in + guard !(excludedViews?.contains(subView) ?? false) else { return [] } var result = subView.getNestedSubviews() as [T] if let view = subView as? T { result.append(view) } return result } } - func getMoleculeViews(filter: ((T) -> Bool)) -> [T] { - return getNestedSubviews().compactMap { + func getMoleculeViews(excludedViews: [UIView]? = nil, filter: ((T) -> Bool)) -> [T] { + return getNestedSubviews(excludedViews: excludedViews).compactMap { filter($0) ? $0 : nil } } diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift index 656d5086..8f8ca005 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift @@ -35,9 +35,4 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate { MVMCoreUIActionHandler.performActionUnstructured(with: closeAction, additionalData: nil, delegateObject: self.delegateObject()) }) } - - open override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - accessibilityElements = [closeButton as Any, tableView as Any] - } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift b/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift deleted file mode 100644 index 89aae858..00000000 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MVMCoreUISession.swift -// MVMCoreUI -// -// Created by Bandaru, Krishna Kishore on 16/09/23. -// Copyright © 2023 Verizon Wireless. All rights reserved. -// - -import Foundation - -@objc public extension MVMCoreUISession { - - @objc func applyGlobalMVMCoreUIBehaviors(to viewController: UIViewController) { - - guard var behaviorController = viewController as? PageBehaviorHandlerProtocol, let delegateObject = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() else { return } - - let accessibilityHandlerBehavior = AccessibilityHandlerBehavior(model: AccessibilityHandlerBehaviorModel(), delegateObject: (delegateObject as! MVMCoreUIDelegateObject)) - - let behaviors = behaviorController.behaviors ?? [] - behaviorController.behaviors = behaviors + [accessibilityHandlerBehavior] - } -} diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m index 02ed8df2..e1ca231f 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -61,7 +61,6 @@ - (void)applyGlobalBehaviorsToController:(nonnull UIViewController *)viewController { // Allow extending frameworks to apply behaviors to add cross cutting concerns to the base controllers. - [self applyGlobalMVMCoreUIBehaviorsTo:viewController]; } @end