From fedd0561753e82241a496a66b465927080b8f798 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 30 Jun 2023 23:58:52 +0530 Subject: [PATCH 01/34] added AccessibilityHandler, MoleculeViewModelProtocol, AccessibilityElementProtocol --- MVMCoreUI.xcodeproj/project.pbxproj | 12 + .../Accessibility/AccessibilityHandler.swift | 223 ++++++++++++++++++ MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift | 8 +- .../Atomic/Atoms/Selectors/ToggleModel.swift | 6 +- .../HeadlineBodyToggleModel.swift | 4 +- .../Protocols/MoleculeViewProtocol.swift | 5 + .../Protocols/AccessibilityProtocol.swift | 5 + MVMCoreUI/OtherHandlers/CoreUIObject.swift | 9 +- 8 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 MVMCoreUI/Accessibility/AccessibilityHandler.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index f0e4f4f6..06d4c8ef 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ 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 */; }; + 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 */; }; 8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */; }; @@ -753,6 +754,7 @@ 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 = ""; }; + 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 = ""; }; 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextBodyTextModel.swift; sourceTree = ""; }; @@ -1449,6 +1451,14 @@ path = OneColumn; sourceTree = ""; }; + 7199C8142A4F3A40001568B7 /* Accessibility */ = { + isa = PBXGroup; + children = ( + 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */, + ); + path = Accessibility; + sourceTree = ""; + }; 8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = { isa = PBXGroup; children = ( @@ -1972,6 +1982,7 @@ D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */ = { isa = PBXGroup; children = ( + 7199C8142A4F3A40001568B7 /* Accessibility */, 01F2C1FC27C81F9700DC3D36 /* Managers */, D2ED27D8254B0C1F00A1C293 /* Alerts */, 27F973512466071600CAB5C5 /* Behaviors */, @@ -2827,6 +2838,7 @@ 0A7ECC702441001C00C828E8 /* UIToolbar+Extension.swift in Sources */, D29DF26D21E6AA0B003B2FB9 /* FLAnimatedImageView.m in Sources */, AA3561AC24C9684400452EB1 /* ListRightVariableRightCaretAllTextAndLinksModel.swift in Sources */, + 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */, D209234F244F77FD0044AD09 /* ThreeLayerCenterTemplate.swift in Sources */, 525019E52406852100EED91C /* ListFourColumnDataUsageDividerModel.swift in Sources */, 32D2609624C19E2100B56344 /* LockupsPlanSMLXL.swift in Sources */, diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift new file mode 100644 index 00000000..3ee9c3f7 --- /dev/null +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -0,0 +1,223 @@ +// +// AccessibilityHandler.swift +// MVMCoreUI +// +// Created by Bandaru, Krishna Kishore on 30/06/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import Combine + +public enum AccessibilityNotificationType { + + case controllerChanged, layoutChanged, screenChanged, announcement + + //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. + //https://developer.apple.com/forums/thread/132699, + //https://developer.apple.com/forums/thread/655359 + //By default from iOS 13+ focus is getting shifted to first interactive element inside viewcontroller not to the navigationitem left barbutton item so posting layoutChanged notification with delay to push to leftbarbutton item on new screen push + var delay: Double { + switch self { + case .controllerChanged: + return 1.5 + case .screenChanged, .layoutChanged: + return 0.0 + default: + return 0.0 + } + } + + var accessibilityNotification: UIAccessibility.Notification { + switch self { + case .announcement: + return .announcement + case .screenChanged: + return .screenChanged + case .layoutChanged, .controllerChanged: + return .layoutChanged + } + } +} + +public class AccessbilityOperation: MVMCoreOperation { + + let argument: Any? + let notificationType: AccessibilityNotificationType + private var timerSource: DispatchSourceTimer? + + public init(notificationType: AccessibilityNotificationType, argument: Any?) { + self.notificationType = notificationType + self.argument = argument + } + + public override func main() { + Task { @MainActor in + guard UIAccessibility.isVoiceOverRunning, !checkAndHandleForCancellation() else { + stop() + return + } + timerSource = DispatchSource.makeTimerSource() + timerSource?.setEventHandler { [weak self] in + if !(self?.isCancelled ?? false), let notification = self?.notificationType.accessibilityNotification { + UIAccessibility.post(notification: notification, argument: self?.argument) + self?.markAsFinished() + } else { + self?.stop() + } + } + timerSource?.schedule(deadline: .now() + notificationType.delay) + timerSource?.activate() + } + } + + public func stop() { + guard isCancelled else { return } + timerSource?.cancel() + markAsFinished() + } +} + +open class AccessibilityHandler { + + public static func shared() -> Self? { + guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } + return MVMCoreActionUtility.fatalClassCheck(object: shared) + } + public let webPageNavigated = PassthroughSubject() + + private var accessibilityOperationQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + private var anyCancellable: Set = [] + private weak var delegate: MVMCoreViewControllerProtocol? + private var accessibilityId: String? + private var operationType: UINavigationController.Operation? + private var previousAccessiblityElement: Any? + + public init() { + registerWithNotificationCenter() + registerForPageChanges() + registerForFocusChanges() +// registerForTopNotificationsChanges() + registerForWebpageNavigation() + } + + /// Registers with the notification center to know when json is updated. + private func registerWithNotificationCenter() { + NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) + .sink { [weak self] notification in + self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") + }.store(in: &anyCancellable) + } + + /// Registers to know when pages change. + private func registerForPageChanges() { + MVMCoreNavigationHandler.shared()?.addDelegate(self) + } + + private func registerForFocusChanges() { + //Since foucs shifted to other elements cancelling existing focus shift notifications if any + NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) + .sink { [weak self] notification in + self?.cancelAllOperations() + }.store(in: &anyCancellable) + } + + private func registerForTopNotificationsChanges() { + NotificationHandler.shared()?.onNotificationShown + .sink { [weak self] (view, model) in + self?.post(notification: .layoutChanged, argument: view) + }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationDismissed + .sink { [weak self] (view, model) in + self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) + self?.post(notification: .screenChanged, argument: self?.previousAccessiblityElement) + }.store(in: &anyCancellable) + print(anyCancellable) + } + + private func registerForWebpageNavigation() { + webPageNavigated.sink { [weak self] _ in + self?.post(notification: .controllerChanged, argument: self?.getFirstFocusedElementOnScreen()) + }.store(in: &anyCancellable) + } + + private func add(operation: Operation) { + accessibilityOperationQueue.addOperation(operation) + } + + private func cancelAllOperations() { + accessibilityOperationQueue.cancelAllOperations() + } + + public func post(notification type: AccessibilityNotificationType, argument: Any?) { + guard UIAccessibility.isVoiceOverRunning else { return } + let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) + add(operation: accessbilityOperation) + previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) + } + + //Subclass can decide to trigger Accessibility notification on screen change. + open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } +} + +extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { + + public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { + delegate = viewController as? MVMCoreViewControllerProtocol + operationType = navigationController.viewControllers.contains(viewController) ? .pop : .push + } + + public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { + //TODO: - For push operation we are posting accessibility notification to the top left item or the identified accessibility element from pageJSON. Need to check the logic for pop operation + guard UIAccessibility.isVoiceOverRunning, + operationType == .push, + canPostAccessbilityNotification(for: viewController) else { return } + let accessbilityElement = getAccessbilityFocusedElement() + post(notification: .controllerChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + } +} + +extension AccessibilityHandler { + + private func getAccessbilityFocusedElement() -> UIView? { + guard let accessibilityModels: [any AccessibilityElementProtocol] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } + guard !accessibilityModels.isEmpty, + let accessibilityModel = (accessibilityModels.filter { $0.id == accessibilityId }).first as? MoleculeModelProtocol else { + return nil + } + return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in + print("subview: \(subView)") + guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.getMoleculeModel() as? (any AccessibilityElementProtocol), + moleculeModel.id == (accessibilityModel as? (any AccessibilityElementProtocol))?.id else { + return false + } + return true + }.first + } + + //To get first foucs element on the screen + private func getFirstFocusedElementOnScreen() -> Any? { + (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar + } +} + +extension UIView { + + private func getNestedSubviews() -> [T] { + subviews.flatMap { subView -> [T] in + 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 { + filter($0) ? $0 : nil + } + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 85ad8e9c..359df799 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -372,7 +372,8 @@ public typealias ActionBlockConfirmation = () -> (Bool) public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) self.delegateObject = delegateObject - + self.model = model + guard let model = model as? ToggleModel else { return } FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) @@ -418,3 +419,8 @@ extension Toggle { public func horizontalAlignment() -> UIStackView.Alignment { .trailing } } + +extension Toggle: MoleculeViewModelProtocol { + + public func getMoleculeModel() -> MoleculeModelProtocol? { model } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index e8d50851..7336bbad 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,7 +7,7 @@ // -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { +public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, AccessibilityElementProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -30,6 +30,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + public var id: String? //-------------------------------------------------- // MARK: - Keys @@ -52,6 +53,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { case offKnobTintColor case fieldKey case groupName + case id } //-------------------------------------------------- @@ -124,6 +126,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) } public func encode(to encoder: Encoder) throws { @@ -144,5 +147,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) try container.encode(readOnly, forKey: .readOnly) + try container.encodeIfPresent(id, forKey: .id) } } diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift index 800c976b..b9b5f81c 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift @@ -9,12 +9,14 @@ import Foundation -open class HeadlineBodyToggleModel: MoleculeModelProtocol { +open class HeadlineBodyToggleModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { public static var identifier: String = "headlineBodyToggle" public var moleculeName: String = HeadlineBodyToggleModel.identifier open var backgroundColor: Color? open var headlineBody: HeadlineBodyModel open var toggle: ToggleModel + + public var children: [MoleculeModelProtocol] { [headlineBody, toggle] } public init(_ headlineBody: HeadlineBodyModel, _ toggle: ToggleModel) { self.headlineBody = headlineBody diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 624f99a6..47e0ca07 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -101,3 +101,8 @@ public extension ModelRegistry { } } } + +public protocol MoleculeViewModelProtocol: UIView { + + func getMoleculeModel() -> MoleculeModelProtocol? +} diff --git a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift index 6c7ada50..d0501874 100644 --- a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift +++ b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift @@ -12,3 +12,8 @@ import Foundation /// Should return the argument to use for posting a layout change. func getAccessibilityLayoutChangedArgument() -> Any? } + +public protocol AccessibilityElementProtocol: Identifiable { + + var id: String? { get set } +} diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index b96e9b93..eaf59289 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -11,8 +11,13 @@ import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { public var alertHandler: AlertHandler? - public var topNotificationHandler: NotificationHandler? - + public var topNotificationHandler: NotificationHandler? { + didSet { + accessibilityHandler = AccessibilityHandler() + } + } + public var accessibilityHandler: AccessibilityHandler? + open override func defaultInitialSetup() { CoreUIModelMapping.registerObjects() loadHandler = MVMCoreLoadHandler() From 49ba311d26ab7d66834c9b7db3f56ca98f58b55b Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 6 Jul 2023 16:53:59 +0530 Subject: [PATCH 02/34] added delay only for tabbar change & added webPageChanged notificationtype --- .../Accessibility/AccessibilityHandler.swift | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 3ee9c3f7..c1376437 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -8,10 +8,11 @@ import Foundation import Combine +import MVMCore -public enum AccessibilityNotificationType { +public enum AccessibilityNotificationType: String, Codable { - case controllerChanged, layoutChanged, screenChanged, announcement + case controllerChanged, layoutChanged, screenChanged, announcement, webPageChanged //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. //https://developer.apple.com/forums/thread/132699, @@ -36,6 +37,8 @@ public enum AccessibilityNotificationType { return .screenChanged case .layoutChanged, .controllerChanged: return .layoutChanged + case .webPageChanged: + return .layoutChanged } } } @@ -94,14 +97,13 @@ open class AccessibilityHandler { private var anyCancellable: Set = [] private weak var delegate: MVMCoreViewControllerProtocol? private var accessibilityId: String? - private var operationType: UINavigationController.Operation? private var previousAccessiblityElement: Any? public init() { registerWithNotificationCenter() registerForPageChanges() registerForFocusChanges() -// registerForTopNotificationsChanges() + registerForTopNotificationsChanges() registerForWebpageNavigation() } @@ -141,7 +143,7 @@ open class AccessibilityHandler { private func registerForWebpageNavigation() { webPageNavigated.sink { [weak self] _ in - self?.post(notification: .controllerChanged, argument: self?.getFirstFocusedElementOnScreen()) + self?.post(notification: .layoutChanged, argument: self?.getFirstFocusedElementOnScreen()) }.store(in: &anyCancellable) } @@ -153,7 +155,7 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } - public func post(notification type: AccessibilityNotificationType, argument: Any?) { + public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) add(operation: accessbilityOperation) @@ -168,16 +170,17 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { delegate = viewController as? MVMCoreViewControllerProtocol - operationType = navigationController.viewControllers.contains(viewController) ? .pop : .push } public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { - //TODO: - For push operation we are posting accessibility notification to the top left item or the identified accessibility element from pageJSON. Need to check the logic for pop operation guard UIAccessibility.isVoiceOverRunning, - operationType == .push, canPostAccessbilityNotification(for: viewController) else { return } + var navigationOperationType: NavigationType = .push + if let presentationStyle = delegate?.loadObject??.pageJSON?.optionalStringForKey(KeyPresentationStyle) ?? delegate?.loadObject??.requestParameters?.actionMap?.optionalStringForKey(KeyPresentationStyle), presentationStyle == "root" { + navigationOperationType = .set //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. + } let accessbilityElement = getAccessbilityFocusedElement() - post(notification: .controllerChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + post(notification: navigationOperationType == .set ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) } } @@ -190,7 +193,6 @@ extension AccessibilityHandler { return nil } return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in - print("subview: \(subView)") guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.getMoleculeModel() as? (any AccessibilityElementProtocol), moleculeModel.id == (accessibilityModel as? (any AccessibilityElementProtocol))?.id else { return false From 21a45203a33f095db9216717189bcf36e028e7b5 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 6 Jul 2023 20:23:43 +0530 Subject: [PATCH 03/34] added temp fix for updated operation type logic --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index c1376437..729b3881 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -175,12 +175,13 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { guard UIAccessibility.isVoiceOverRunning, canPostAccessbilityNotification(for: viewController) else { return } - var navigationOperationType: NavigationType = .push + //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count + /*var navigationOperationType: NavigationType = .push if let presentationStyle = delegate?.loadObject??.pageJSON?.optionalStringForKey(KeyPresentationStyle) ?? delegate?.loadObject??.requestParameters?.actionMap?.optionalStringForKey(KeyPresentationStyle), presentationStyle == "root" { - navigationOperationType = .set //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. - } + navigationOperationType = .set + }*/ let accessbilityElement = getAccessbilityFocusedElement() - post(notification: navigationOperationType == .set ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) } } From ebb2c35c555de095950dcf4e3e38345e11188025 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 7 Jul 2023 18:21:27 +0530 Subject: [PATCH 04/34] setting accessibilityId to nil --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 729b3881..3fcb30c1 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -182,6 +182,7 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { }*/ let accessbilityElement = getAccessbilityFocusedElement() post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + accessibilityId = nil } } From 3d556a850cace92b2e056e0dabda02abc05278b0 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 14 Jul 2023 22:46:22 +0530 Subject: [PATCH 05/34] enhancements for top alert post notification for accessibility --- .../Accessibility/AccessibilityHandler.swift | 67 +++++++++++++------ .../NotificationMoleculeModel.swift | 6 +- .../NotificationContainerView.swift | 14 ---- MVMCoreUI/OtherHandlers/CoreUIObject.swift | 6 +- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 3fcb30c1..a7e0f245 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -9,10 +9,11 @@ import Foundation import Combine import MVMCore +import WebKit public enum AccessibilityNotificationType: String, Codable { - case controllerChanged, layoutChanged, screenChanged, announcement, webPageChanged + case controllerChanged, layoutChanged, screenChanged, announcement, webPageChanged, webPageLoaded //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. //https://developer.apple.com/forums/thread/132699, @@ -20,7 +21,7 @@ public enum AccessibilityNotificationType: String, Codable { //By default from iOS 13+ focus is getting shifted to first interactive element inside viewcontroller not to the navigationitem left barbutton item so posting layoutChanged notification with delay to push to leftbarbutton item on new screen push var delay: Double { switch self { - case .controllerChanged: + case .controllerChanged, .webPageLoaded: return 1.5 case .screenChanged, .layoutChanged: return 0.0 @@ -37,7 +38,7 @@ public enum AccessibilityNotificationType: String, Codable { return .screenChanged case .layoutChanged, .controllerChanged: return .layoutChanged - case .webPageChanged: + case .webPageChanged, .webPageLoaded: return .layoutChanged } } @@ -87,7 +88,8 @@ open class AccessibilityHandler { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } - public let webPageNavigated = PassthroughSubject() + public weak var delegate: MVMCoreViewControllerProtocol? + public var previousAccessiblityElement: Any? private var accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() @@ -95,16 +97,15 @@ open class AccessibilityHandler { return queue }() private var anyCancellable: Set = [] - private weak var delegate: MVMCoreViewControllerProtocol? private var accessibilityId: String? - private var previousAccessiblityElement: Any? + private var announcementText: String? + private var hasTopNotitificationInPage: Bool = false public init() { registerWithNotificationCenter() registerForPageChanges() registerForFocusChanges() registerForTopNotificationsChanges() - registerForWebpageNavigation() } /// Registers with the notification center to know when json is updated. @@ -112,6 +113,7 @@ open class AccessibilityHandler { NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .sink { [weak self] notification in self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") + self?.announcementText = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("announcementText") }.store(in: &anyCancellable) } @@ -129,22 +131,31 @@ open class AccessibilityHandler { } private func registerForTopNotificationsChanges() { + NotificationHandler.shared()?.onNotificationWillShow.sink { [weak self] (_, model) in + self?.hasTopNotitificationInPage = true + self?.capturePreviousFocusElement(for: model.molecule) + }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationShown .sink { [weak self] (view, model) in self?.post(notification: .layoutChanged, argument: view) }.store(in: &anyCancellable) - NotificationHandler.shared()?.onNotificationDismissed + NotificationHandler.shared()?.onNotificationWillDismiss .sink { [weak self] (view, model) in self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) - self?.post(notification: .screenChanged, argument: self?.previousAccessiblityElement) + }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationDismissed + .sink { [weak self] (view, model) in + self?.postAccessbilityToPrevElement(for: model.molecule) }.store(in: &anyCancellable) print(anyCancellable) } - private func registerForWebpageNavigation() { - webPageNavigated.sink { [weak self] _ in - self?.post(notification: .layoutChanged, argument: self?.getFirstFocusedElementOnScreen()) - }.store(in: &anyCancellable) + open func capturePreviousFocusElement(for model: MoleculeModelProtocol) { + previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) + } + + open func postAccessbilityToPrevElement(for model: MoleculeModelProtocol) { + post(notification: .layoutChanged, argument: previousAccessiblityElement) } private func add(operation: Operation) { @@ -155,11 +166,19 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } + open func post(webpageChanged type: AccessibilityNotificationType, argument: Any? = nil) { + post(notification: type, argument: argument) + } + public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) add(operation: accessbilityOperation) - previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) + } + + //To get first foucs element on the screen + open func getFirstFocusedElementOnScreen() -> Any? { + (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar } //Subclass can decide to trigger Accessibility notification on screen change. @@ -169,7 +188,12 @@ open class AccessibilityHandler { extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { + previousAccessiblityElement = nil delegate = viewController as? MVMCoreViewControllerProtocol + if let announcementText { + let accessbilityOperation = AccessbilityOperation(notificationType: .announcement, argument: announcementText) + add(operation: accessbilityOperation) + } } public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { @@ -180,9 +204,13 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { if let presentationStyle = delegate?.loadObject??.pageJSON?.optionalStringForKey(KeyPresentationStyle) ?? delegate?.loadObject??.requestParameters?.actionMap?.optionalStringForKey(KeyPresentationStyle), presentationStyle == "root" { navigationOperationType = .set }*/ - let accessbilityElement = getAccessbilityFocusedElement() - post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) - accessibilityId = nil + if hasTopNotitificationInPage { + previousAccessiblityElement = getFirstFocusedElementOnScreen() + } else { + let accessbilityElement = getAccessbilityFocusedElement() + post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + accessibilityId = nil + } } } @@ -202,11 +230,6 @@ extension AccessibilityHandler { return true }.first } - - //To get first foucs element on the screen - private func getFirstFocusedElementOnScreen() -> Any? { - (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar - } } extension UIView { diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift index d26bb06c..9143e1c3 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift @@ -7,7 +7,7 @@ // -open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol { +open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol, AccessibilityElementProtocol { /** The style of the notification: @@ -35,18 +35,20 @@ open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol { public var button: ButtonModel? public var closeButton: NotificationXButtonModel? public var style: NotificationMoleculeModel.Style = .success + public var id: String? //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- - public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) { + public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil, id: String? = nil) { self.headline = headline self.style = style self.backgroundColor = backgroundColor self.body = body self.button = button self.closeButton = closeButton + self.id = id super.init() } diff --git a/MVMCoreUI/Notification/NotificationContainerView.swift b/MVMCoreUI/Notification/NotificationContainerView.swift index 62d05a9d..0fee99e0 100644 --- a/MVMCoreUI/Notification/NotificationContainerView.swift +++ b/MVMCoreUI/Notification/NotificationContainerView.swift @@ -25,16 +25,6 @@ public class NotificationContainerView: UIView { super.init(coder: coder) setupView() } - - /// Posts a layout change with taking the arguments from the view following the AccessibilityProtocol. - private func updateAccessibilityForTopAlert(_ view: UIView) { - // Update accessibility with top alert - var accessibilityArgument: Any? = view - if let view = view as? AccessibilityProtocol { - accessibilityArgument = view.getAccessibilityLayoutChangedArgument() - } - UIAccessibility.post(notification: .layoutChanged, argument: accessibilityArgument) - } } extension NotificationContainerView: NotificationTransitionDelegateProtocol { @@ -56,7 +46,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { self.superview?.layoutIfNeeded() } completion: { finished in self.superview?.layoutIfNeeded() - self.updateAccessibilityForTopAlert(notification) continuation.resume() } } @@ -64,14 +53,11 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { @MainActor public func hide(notification: UIView) async { - // accessibility - below line added to notify VI user through voiceover user when the top alert is closed - UIAccessibility.post(notification: .screenChanged, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) await withCheckedContinuation { continuation in UIView.animate(withDuration: 0.5) { self.height.isActive = true self.superview?.layoutIfNeeded() } completion: { finished in - UIAccessibility.post(notification: .layoutChanged, argument: nil) self.currentNotificationView?.removeFromSuperview() self.currentNotificationView = nil continuation.resume() diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index eaf59289..7480b3ad 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -11,11 +11,7 @@ import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { public var alertHandler: AlertHandler? - public var topNotificationHandler: NotificationHandler? { - didSet { - accessibilityHandler = AccessibilityHandler() - } - } + public var topNotificationHandler: NotificationHandler? public var accessibilityHandler: AccessibilityHandler? open override func defaultInitialSetup() { From 646210f1a3917e5086fb6c5dcfb057933e968f9c Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 21 Jul 2023 12:43:15 +0530 Subject: [PATCH 06/34] updated operation type --- .../Accessibility/AccessibilityHandler.swift | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index a7e0f245..63fb3f74 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -21,8 +21,10 @@ public enum AccessibilityNotificationType: String, Codable { //By default from iOS 13+ focus is getting shifted to first interactive element inside viewcontroller not to the navigationitem left barbutton item so posting layoutChanged notification with delay to push to leftbarbutton item on new screen push var delay: Double { switch self { - case .controllerChanged, .webPageLoaded: + case .controllerChanged: return 1.5 + case .webPageLoaded: + return 2.0 case .screenChanged, .layoutChanged: return 0.0 default: @@ -44,35 +46,40 @@ public enum AccessibilityNotificationType: String, Codable { } } +public typealias ArgumentHandler = ((NavigationOperationType?) -> Any?) + public class AccessbilityOperation: MVMCoreOperation { - let argument: Any? - let notificationType: AccessibilityNotificationType + private let operationType: NavigationOperationType + private let argumentHandler: ArgumentHandler? + private let notificationType: AccessibilityNotificationType private var timerSource: DispatchSourceTimer? - public init(notificationType: AccessibilityNotificationType, argument: Any?) { + public init(notificationType: AccessibilityNotificationType, operationType: NavigationOperationType = .default, argumentHandler: ArgumentHandler?) { self.notificationType = notificationType - self.argument = argument + self.argumentHandler = argumentHandler + self.operationType = operationType } public override func main() { - Task { @MainActor in - guard UIAccessibility.isVoiceOverRunning, !checkAndHandleForCancellation() else { - stop() - return - } - timerSource = DispatchSource.makeTimerSource() - timerSource?.setEventHandler { [weak self] in + guard UIAccessibility.isVoiceOverRunning, !checkAndHandleForCancellation() else { + stop() + return + } + timerSource = DispatchSource.makeTimerSource() + timerSource?.setEventHandler { + Task { @MainActor [weak self] in if !(self?.isCancelled ?? false), let notification = self?.notificationType.accessibilityNotification { - UIAccessibility.post(notification: notification, argument: self?.argument) + print("argumentHandler \(self?.argumentHandler?(self?.operationType))") + UIAccessibility.post(notification: notification, argument: self?.argumentHandler?(self?.operationType)) self?.markAsFinished() } else { self?.stop() } } - timerSource?.schedule(deadline: .now() + notificationType.delay) - timerSource?.activate() } + timerSource?.schedule(deadline: .now() + notificationType.delay) + timerSource?.activate() } public func stop() { @@ -82,6 +89,8 @@ public class AccessbilityOperation: MVMCoreOperation { } } +public enum NavigationOperationType { case `default`, tab } + open class AccessibilityHandler { public static func shared() -> Self? { @@ -126,15 +135,18 @@ open class AccessibilityHandler { //Since foucs shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) .sink { [weak self] notification in + print("testing \(UIAccessibility.focusedElement(using: .notificationVoiceOver))") + print("testing \(notification.userInfo)") self?.cancelAllOperations() }.store(in: &anyCancellable) } private func registerForTopNotificationsChanges() { - NotificationHandler.shared()?.onNotificationWillShow.sink { [weak self] (_, model) in - self?.hasTopNotitificationInPage = true - self?.capturePreviousFocusElement(for: model.molecule) - }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationWillShow + .sink { [weak self] (_, model) in + self?.hasTopNotitificationInPage = true + self?.capturePreviousFocusElement(for: model.molecule) + }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationShown .sink { [weak self] (view, model) in self?.post(notification: .layoutChanged, argument: view) @@ -147,7 +159,6 @@ open class AccessibilityHandler { .sink { [weak self] (view, model) in self?.postAccessbilityToPrevElement(for: model.molecule) }.store(in: &anyCancellable) - print(anyCancellable) } open func capturePreviousFocusElement(for model: MoleculeModelProtocol) { @@ -166,13 +177,15 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } - open func post(webpageChanged type: AccessibilityNotificationType, argument: Any? = nil) { - post(notification: type, argument: argument) + open func post(webpageChanged type: AccessibilityNotificationType) { + post(notification: type) } - public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { + public func post(notification type: AccessibilityNotificationType, operationType: NavigationOperationType = .default, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } - let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) + let accessbilityOperation = AccessbilityOperation(notificationType: type, operationType: operationType) { [weak self] in + ($0 == .tab) ? self?.getFirstFocusedElementOnScreen() : argument + } add(operation: accessbilityOperation) } @@ -187,16 +200,17 @@ open class AccessibilityHandler { extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { - public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { + open func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { previousAccessiblityElement = nil delegate = viewController as? MVMCoreViewControllerProtocol if let announcementText { - let accessbilityOperation = AccessbilityOperation(notificationType: .announcement, argument: announcementText) + let accessbilityOperation = AccessbilityOperation(notificationType: .announcement) { _ in announcementText } add(operation: accessbilityOperation) } } - public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { + @MainActor + open func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { guard UIAccessibility.isVoiceOverRunning, canPostAccessbilityNotification(for: viewController) else { return } //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count @@ -208,7 +222,8 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { previousAccessiblityElement = getFirstFocusedElementOnScreen() } else { let accessbilityElement = getAccessbilityFocusedElement() - post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + let operationType: NavigationOperationType = navigationController.children.count == 1 ? .tab : .default //TODO: - need to identify the operationType + post(notification: operationType == .tab ? .controllerChanged : .layoutChanged, operationType: operationType, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) accessibilityId = nil } } From 9bbcd6a8eab896f17aa2413d479a03aa733e56a9 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 26 Jul 2023 20:13:08 +0530 Subject: [PATCH 07/34] removed accessibility notification for webpages. --- .../Accessibility/AccessibilityHandler.swift | 54 ++++++------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 63fb3f74..cea6ffc5 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -13,7 +13,7 @@ import WebKit public enum AccessibilityNotificationType: String, Codable { - case controllerChanged, layoutChanged, screenChanged, announcement, webPageChanged, webPageLoaded + case controllerChanged, layoutChanged, screenChanged, announcement //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. //https://developer.apple.com/forums/thread/132699, @@ -23,10 +23,6 @@ public enum AccessibilityNotificationType: String, Codable { switch self { case .controllerChanged: return 1.5 - case .webPageLoaded: - return 2.0 - case .screenChanged, .layoutChanged: - return 0.0 default: return 0.0 } @@ -40,8 +36,6 @@ public enum AccessibilityNotificationType: String, Codable { return .screenChanged case .layoutChanged, .controllerChanged: return .layoutChanged - case .webPageChanged, .webPageLoaded: - return .layoutChanged } } } @@ -50,15 +44,13 @@ public typealias ArgumentHandler = ((NavigationOperationType?) -> Any?) public class AccessbilityOperation: MVMCoreOperation { - private let operationType: NavigationOperationType - private let argumentHandler: ArgumentHandler? + private let argument: Any? private let notificationType: AccessibilityNotificationType private var timerSource: DispatchSourceTimer? - public init(notificationType: AccessibilityNotificationType, operationType: NavigationOperationType = .default, argumentHandler: ArgumentHandler?) { + public init(notificationType: AccessibilityNotificationType, argument: Any?) { self.notificationType = notificationType - self.argumentHandler = argumentHandler - self.operationType = operationType + self.argument = argument } public override func main() { @@ -70,8 +62,7 @@ public class AccessbilityOperation: MVMCoreOperation { timerSource?.setEventHandler { Task { @MainActor [weak self] in if !(self?.isCancelled ?? false), let notification = self?.notificationType.accessibilityNotification { - print("argumentHandler \(self?.argumentHandler?(self?.operationType))") - UIAccessibility.post(notification: notification, argument: self?.argumentHandler?(self?.operationType)) + UIAccessibility.post(notification: notification, argument: self?.argument) self?.markAsFinished() } else { self?.stop() @@ -99,13 +90,13 @@ open class AccessibilityHandler { } public weak var delegate: MVMCoreViewControllerProtocol? public var previousAccessiblityElement: Any? + public var anyCancellable: Set = [] private var accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 return queue }() - private var anyCancellable: Set = [] private var accessibilityId: String? private var announcementText: String? private var hasTopNotitificationInPage: Bool = false @@ -127,7 +118,7 @@ open class AccessibilityHandler { } /// Registers to know when pages change. - private func registerForPageChanges() { + open func registerForPageChanges() { MVMCoreNavigationHandler.shared()?.addDelegate(self) } @@ -135,8 +126,6 @@ open class AccessibilityHandler { //Since foucs shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) .sink { [weak self] notification in - print("testing \(UIAccessibility.focusedElement(using: .notificationVoiceOver))") - print("testing \(notification.userInfo)") self?.cancelAllOperations() }.store(in: &anyCancellable) } @@ -145,7 +134,7 @@ open class AccessibilityHandler { NotificationHandler.shared()?.onNotificationWillShow .sink { [weak self] (_, model) in self?.hasTopNotitificationInPage = true - self?.capturePreviousFocusElement(for: model.molecule) + self?.capturePreviousFocusElement() }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationShown .sink { [weak self] (view, model) in @@ -157,15 +146,15 @@ open class AccessibilityHandler { }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationDismissed .sink { [weak self] (view, model) in - self?.postAccessbilityToPrevElement(for: model.molecule) + self?.postAccessbilityToPrevElement() }.store(in: &anyCancellable) } - open func capturePreviousFocusElement(for model: MoleculeModelProtocol) { + open func capturePreviousFocusElement() { previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) } - open func postAccessbilityToPrevElement(for model: MoleculeModelProtocol) { + open func postAccessbilityToPrevElement() { post(notification: .layoutChanged, argument: previousAccessiblityElement) } @@ -177,15 +166,9 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } - open func post(webpageChanged type: AccessibilityNotificationType) { - post(notification: type) - } - - public func post(notification type: AccessibilityNotificationType, operationType: NavigationOperationType = .default, argument: Any? = nil) { + public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } - let accessbilityOperation = AccessbilityOperation(notificationType: type, operationType: operationType) { [weak self] in - ($0 == .tab) ? self?.getFirstFocusedElementOnScreen() : argument - } + let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) add(operation: accessbilityOperation) } @@ -204,8 +187,7 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { previousAccessiblityElement = nil delegate = viewController as? MVMCoreViewControllerProtocol if let announcementText { - let accessbilityOperation = AccessbilityOperation(notificationType: .announcement) { _ in announcementText } - add(operation: accessbilityOperation) + post(notification: .announcement, argument: announcementText) } } @@ -213,17 +195,13 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { open func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { guard UIAccessibility.isVoiceOverRunning, canPostAccessbilityNotification(for: viewController) else { return } - //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count - /*var navigationOperationType: NavigationType = .push - if let presentationStyle = delegate?.loadObject??.pageJSON?.optionalStringForKey(KeyPresentationStyle) ?? delegate?.loadObject??.requestParameters?.actionMap?.optionalStringForKey(KeyPresentationStyle), presentationStyle == "root" { - navigationOperationType = .set - }*/ + //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count. If we have top notification in page on pageLoad, we have postnotification for shifting the focus so in this case we are not posting accessiblity notifcation if hasTopNotitificationInPage { previousAccessiblityElement = getFirstFocusedElementOnScreen() } else { let accessbilityElement = getAccessbilityFocusedElement() let operationType: NavigationOperationType = navigationController.children.count == 1 ? .tab : .default //TODO: - need to identify the operationType - post(notification: operationType == .tab ? .controllerChanged : .layoutChanged, operationType: operationType, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + post(notification: operationType == .tab ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) accessibilityId = nil } } From e61d7c5b076b25ff375d185cc4d5630ccc9cd7f2 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 26 Jul 2023 20:31:06 +0530 Subject: [PATCH 08/34] reverted changes in notification molecule model --- .../Molecules/TopNotification/NotificationMoleculeModel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift index 9143e1c3..0f3e546a 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift @@ -35,20 +35,18 @@ open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol, Acc public var button: ButtonModel? public var closeButton: NotificationXButtonModel? public var style: NotificationMoleculeModel.Style = .success - public var id: String? //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- - public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil, id: String? = nil) { + public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) { self.headline = headline self.style = style self.backgroundColor = backgroundColor self.body = body self.button = button self.closeButton = closeButton - self.id = id super.init() } From 21a339f0453753ff19149515c14a1fa3ee5b7634 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 26 Jul 2023 21:06:29 +0530 Subject: [PATCH 09/34] removed AccessibilityElementProtocol --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 1 - .../Molecules/TopNotification/NotificationMoleculeModel.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index cea6ffc5..b476324f 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -191,7 +191,6 @@ extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { } } - @MainActor open func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { guard UIAccessibility.isVoiceOverRunning, canPostAccessbilityNotification(for: viewController) else { return } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift index 0f3e546a..d26bb06c 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift @@ -7,7 +7,7 @@ // -open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol, AccessibilityElementProtocol { +open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol { /** The style of the notification: From f8bc4c11678123079476befa7c97208a984d8fe9 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 2 Aug 2023 17:32:37 +0530 Subject: [PATCH 10/34] added default id to UUID --- MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift | 7 ++++--- .../BaseClasses/Protocols/AccessibilityProtocol.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index 7336bbad..2c07134a 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -30,7 +30,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? - public var id: String? + public var id: String //-------------------------------------------------- // MARK: - Keys @@ -76,9 +76,10 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit // MARK: - Initializer //-------------------------------------------------- - public init(_ state: Bool) { + public init(_ state: Bool, id: String = UUID().uuidString) { self.selected = state baseValue = state + self.id = id } //-------------------------------------------------- @@ -126,7 +127,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - id = try typeContainer.decodeIfPresent(String.self, forKey: .id) + id = try typeContainer.decode(forKey: .id, default: { UUID().uuidString }()) } public func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift index d0501874..2a6456dd 100644 --- a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift +++ b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift @@ -15,5 +15,5 @@ import Foundation public protocol AccessibilityElementProtocol: Identifiable { - var id: String? { get set } + var id: String { get set } } From 32ba75e731c7118e5c7df7af9fa20fd884771e19 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 21 Sep 2023 19:17:06 +0530 Subject: [PATCH 11/34] updated with code review comments & created accessibility handler behaviour --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + .../Accessibility/AccessibilityHandler.swift | 200 ++++++++++-------- MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift | 3 +- .../Atomic/Atoms/Selectors/ToggleModel.swift | 5 +- .../Protocols/MoleculeViewProtocol.swift | 9 +- .../Protocols/AccessibilityProtocol.swift | 5 - MVMCoreUI/OtherHandlers/CoreUIObject.swift | 7 +- .../MVMCoreUISession+Extension.swift | 22 ++ MVMCoreUI/OtherHandlers/MVMCoreUISession.m | 2 + 9 files changed, 151 insertions(+), 106 deletions(-) create mode 100644 MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 4e04dafc..bb67be60 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ 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 */; }; @@ -755,6 +756,7 @@ 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 = ""; }; @@ -2277,6 +2279,7 @@ D2B18B912361E65A00A9AEDC /* CoreUIObject.swift */, D29DF27721E7A533003B2FB9 /* MVMCoreUISession.h */, D29DF27821E7A533003B2FB9 /* MVMCoreUISession.m */, + 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */, D29DF27321E79E81003B2FB9 /* MVMCoreUILoggingHandler.h */, D29DF27421E79E81003B2FB9 /* MVMCoreUILoggingHandler.m */, AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */, @@ -3093,6 +3096,7 @@ 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 b476324f..96664c06 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -11,44 +11,13 @@ import Combine import MVMCore import WebKit -public enum AccessibilityNotificationType: String, Codable { - - case controllerChanged, layoutChanged, screenChanged, announcement - - //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. - //https://developer.apple.com/forums/thread/132699, - //https://developer.apple.com/forums/thread/655359 - //By default from iOS 13+ focus is getting shifted to first interactive element inside viewcontroller not to the navigationitem left barbutton item so posting layoutChanged notification with delay to push to leftbarbutton item on new screen push - var delay: Double { - switch self { - case .controllerChanged: - return 1.5 - default: - return 0.0 - } - } - - var accessibilityNotification: UIAccessibility.Notification { - switch self { - case .announcement: - return .announcement - case .screenChanged: - return .screenChanged - case .layoutChanged, .controllerChanged: - return .layoutChanged - } - } -} - -public typealias ArgumentHandler = ((NavigationOperationType?) -> Any?) - public class AccessbilityOperation: MVMCoreOperation { private let argument: Any? - private let notificationType: AccessibilityNotificationType + private let notificationType: UIAccessibility.Notification private var timerSource: DispatchSourceTimer? - public init(notificationType: AccessibilityNotificationType, argument: Any?) { + public init(notificationType: UIAccessibility.Notification, argument: Any?) { self.notificationType = notificationType self.argument = argument } @@ -61,15 +30,15 @@ public class AccessbilityOperation: MVMCoreOperation { timerSource = DispatchSource.makeTimerSource() timerSource?.setEventHandler { Task { @MainActor [weak self] in - if !(self?.isCancelled ?? false), let notification = self?.notificationType.accessibilityNotification { - UIAccessibility.post(notification: notification, argument: self?.argument) - self?.markAsFinished() - } else { + guard let self = self, !self.isCancelled else { self?.stop() + return } + UIAccessibility.post(notification: self.notificationType, argument: self.argument) + self.markAsFinished() } } - timerSource?.schedule(deadline: .now() + notificationType.delay) + timerSource?.schedule(deadline: .now()) timerSource?.activate() } @@ -80,17 +49,16 @@ public class AccessbilityOperation: MVMCoreOperation { } } -public enum NavigationOperationType { case `default`, tab } - open class AccessibilityHandler { public static func shared() -> Self? { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } - public weak var delegate: MVMCoreViewControllerProtocol? + public var previousAccessiblityElement: Any? public var anyCancellable: Set = [] + public weak var delegate: MVMCoreViewControllerProtocol? private var accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() @@ -100,16 +68,16 @@ open class AccessibilityHandler { private var accessibilityId: String? private var announcementText: String? private var hasTopNotitificationInPage: Bool = false - + public init() { - registerWithNotificationCenter() + registerWithResponseLoaded() registerForPageChanges() registerForFocusChanges() - registerForTopNotificationsChanges() } - - /// Registers with the notification center to know when json is updated. - private func registerWithNotificationCenter() { + + // MARK: - Register with Accessibility Handler listeners + /// Registers with the notification center to know when json is updated and to capture previous accessbility focused id & announcment text + private func registerWithResponseLoaded() { NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .sink { [weak self] notification in self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") @@ -117,20 +85,15 @@ open class AccessibilityHandler { }.store(in: &anyCancellable) } - /// Registers to know when pages change. - open func registerForPageChanges() { - MVMCoreNavigationHandler.shared()?.addDelegate(self) - } - private func registerForFocusChanges() { //Since foucs shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) - .sink { [weak self] notification in + .sink { [weak self] _ in self?.cancelAllOperations() }.store(in: &anyCancellable) } - - private func registerForTopNotificationsChanges() { + + func registerForTopNotificationsChanges() { NotificationHandler.shared()?.onNotificationWillShow .sink { [weak self] (_, model) in self?.hasTopNotitificationInPage = true @@ -150,6 +113,49 @@ open class AccessibilityHandler { }.store(in: &anyCancellable) } + /// Registers to know when pages change. + open func registerForPageChanges() { + NavigationHandler.shared() + .onNavigation + .sink { [self] (event, operation) in + switch event { + case .willNavigate: + willNavigate(operation) + @unknown default: + break + } + }.store(in: &anyCancellable) + } + + private func willNavigate(_ operation: NavigationOperation) { + previousAccessiblityElement = nil + if let announcementText { + post(notification: .announcement, argument: announcementText) + } + if let subNavManagerController = operation.toNavigationControllerViewControllers?.last as? SubNavManagerController { + delegate = subNavManagerController.getCurrentViewController() as? MVMCoreViewControllerProtocol + } else { + delegate = operation.toNavigationControllerViewControllers?.last as? MVMCoreViewControllerProtocol + } + } + + /*private func didNavigate(_ operation: NavigationOperation) { + guard UIAccessibility.isVoiceOverRunning, + let viewController = operation.toNavigationControllerViewControllers?.last, + canPostAccessbilityNotification(for: viewController) else { return } + delegate = viewController as? MVMCoreViewControllerProtocol + guard let view = operation.toNavigationControllerViewControllers?.last?.view else { return } + view.accessibilityElements = getAccessibilityElementsOnScreen() + if hasTopNotitificationInPage { + previousAccessiblityElement = getFirstFocusedElementOnScreen() + } else { + let accessbilityElement = getUIElementBasedOn(id: accessibilityId) + post(notification: .screenChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + accessibilityId = nil + } + }*/ + + // MARK: - Accessibility Handler operation events open func capturePreviousFocusElement() { previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) } @@ -166,57 +172,27 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } - public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { + public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) add(operation: accessbilityOperation) } - //To get first foucs element on the screen + //To get first focus element on the screen open func getFirstFocusedElementOnScreen() -> Any? { (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar } //Subclass can decide to trigger Accessibility notification on screen change. open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } -} - -extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { - open func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { - previousAccessiblityElement = nil - delegate = viewController as? MVMCoreViewControllerProtocol - if let announcementText { - post(notification: .announcement, argument: announcementText) - } - } - - open func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { - guard UIAccessibility.isVoiceOverRunning, - canPostAccessbilityNotification(for: viewController) else { return } - //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count. If we have top notification in page on pageLoad, we have postnotification for shifting the focus so in this case we are not posting accessiblity notifcation - if hasTopNotitificationInPage { - previousAccessiblityElement = getFirstFocusedElementOnScreen() - } else { - let accessbilityElement = getAccessbilityFocusedElement() - let operationType: NavigationOperationType = navigationController.children.count == 1 ? .tab : .default //TODO: - need to identify the operationType - post(notification: operationType == .tab ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) - accessibilityId = nil - } - } -} - -extension AccessibilityHandler { - - private func getAccessbilityFocusedElement() -> UIView? { - guard let accessibilityModels: [any AccessibilityElementProtocol] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } - guard !accessibilityModels.isEmpty, - let accessibilityModel = (accessibilityModels.filter { $0.id == accessibilityId }).first as? MoleculeModelProtocol else { - return nil - } + func getPreDefinedFocusedElementIfAny() -> UIView? { + guard let accessibilityId, let models: [any Identifiable] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } + guard !models.isEmpty, + let model = (models.filter { ($0.id as? String) == accessibilityId }).first else { return nil } return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in - guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.getMoleculeModel() as? (any AccessibilityElementProtocol), - moleculeModel.id == (accessibilityModel as? (any AccessibilityElementProtocol))?.id else { + guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel as? (any Identifiable), + (moleculeModel.id as? String) == (model.id as? String) else { return false } return true @@ -224,6 +200,44 @@ extension 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 { + + var shouldAllowMultipleInstances = false + static var identifier = "accessibilityHandlerBehaviorModel" +} + +class AccessibilityHandlerBehavior: PageVisibilityBehavior { + + required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } + + public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + let viewController = updateAccessibilityViews(delegateObject) + AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() ?? AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) + } + + private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) -> UIViewController? { + var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController] + var viewController: UIViewController? + if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { + var managerControllerViews = [Any?]() + managerControllerViews.append(managerController.navigationController) + managerControllerViews.append(managerController.tabs) + managerControllerViews.append(contentsOf: managerController.view.subviews) + managerController.view.accessibilityElements = managerControllerViews.compactMap { $0 } + } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { + accessibilityElements.append(controller.navigationController) + accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + controller.view.accessibilityElements = accessibilityElements + viewController = controller + } + return viewController + } +} + +// MARK: - Helpers extension UIView { private func getNestedSubviews() -> [T] { diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 359df799..d84efad6 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -372,7 +372,6 @@ public typealias ActionBlockConfirmation = () -> (Bool) public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) self.delegateObject = delegateObject - self.model = model guard let model = model as? ToggleModel else { return } @@ -422,5 +421,5 @@ extension Toggle { extension Toggle: MoleculeViewModelProtocol { - public func getMoleculeModel() -> MoleculeModelProtocol? { model } + public var moleculeModel: MoleculeModelProtocol? { model } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index 3757dee5..fc267cff 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,7 +7,7 @@ // -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, AccessibilityElementProtocol { +public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Identifiable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -32,7 +32,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? - public var id: String //-------------------------------------------------- // MARK: - Keys @@ -56,7 +55,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit case offKnobTintColor case fieldKey case groupName - case id } //-------------------------------------------------- @@ -132,7 +130,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - id = try typeContainer.decode(forKey: .id, default: { UUID().uuidString }()) } public func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 47e0ca07..dbd6b2df 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -104,5 +104,12 @@ public extension ModelRegistry { public protocol MoleculeViewModelProtocol: UIView { - func getMoleculeModel() -> MoleculeModelProtocol? + var moleculeModel: MoleculeModelProtocol? { get } +} + +extension MoleculeViewModelProtocol { + + var moleculeModel: MoleculeModelProtocol? { + get { nil } + } } diff --git a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift index 2a6456dd..6c7ada50 100644 --- a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift +++ b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift @@ -12,8 +12,3 @@ import Foundation /// Should return the argument to use for posting a layout change. func getAccessibilityLayoutChangedArgument() -> Any? } - -public protocol AccessibilityElementProtocol: Identifiable { - - var id: String { get set } -} diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index 7480b3ad..dd82a34f 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -11,7 +11,11 @@ import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { public var alertHandler: AlertHandler? - public var topNotificationHandler: NotificationHandler? + public var topNotificationHandler: NotificationHandler? { + didSet { + accessibilityHandler?.registerForTopNotificationsChanges() + } + } public var accessibilityHandler: AccessibilityHandler? open override func defaultInitialSetup() { @@ -24,5 +28,6 @@ import MVMCore viewControllerMapping = MVMCoreUIViewControllerMappingObject() loggingDelegate = MVMCoreUILoggingHandler() alertHandler = AlertHandler() + accessibilityHandler = AccessibilityHandler() } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift b/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift new file mode 100644 index 00000000..89aae858 --- /dev/null +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift @@ -0,0 +1,22 @@ +// +// 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 42a3e769..02ed8df2 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -9,6 +9,7 @@ #import "MVMCoreUISession.h" #import "MFLoadingViewController.h" #import "NSLayoutConstraint+MFConvenience.h" +#import @import MVMCore.MVMCoreObject; @interface MVMCoreUISession () @@ -60,6 +61,7 @@ - (void)applyGlobalBehaviorsToController:(nonnull UIViewController *)viewController { // Allow extending frameworks to apply behaviors to add cross cutting concerns to the base controllers. + [self applyGlobalMVMCoreUIBehaviorsTo:viewController]; } @end From e9dc771eea5c8d0e50d76cd362af9ed552bae184 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 22 Sep 2023 17:16:09 +0530 Subject: [PATCH 12/34] removed unused code & added support for predefinedfocused elements from server --- .../Accessibility/AccessibilityHandler.swift | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 96664c06..fde20909 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -65,9 +65,9 @@ open class AccessibilityHandler { queue.maxConcurrentOperationCount = 1 return queue }() - private var accessibilityId: String? + private(set) var accessibilityId: String? private var announcementText: String? - private var hasTopNotitificationInPage: Bool = false + private(set) var hasTopNotitificationInPage: Bool = false public init() { registerWithResponseLoaded() @@ -121,7 +121,7 @@ open class AccessibilityHandler { switch event { case .willNavigate: willNavigate(operation) - @unknown default: + default: break } }.store(in: &anyCancellable) @@ -138,22 +138,6 @@ open class AccessibilityHandler { delegate = operation.toNavigationControllerViewControllers?.last as? MVMCoreViewControllerProtocol } } - - /*private func didNavigate(_ operation: NavigationOperation) { - guard UIAccessibility.isVoiceOverRunning, - let viewController = operation.toNavigationControllerViewControllers?.last, - canPostAccessbilityNotification(for: viewController) else { return } - delegate = viewController as? MVMCoreViewControllerProtocol - guard let view = operation.toNavigationControllerViewControllers?.last?.view else { return } - view.accessibilityElements = getAccessibilityElementsOnScreen() - if hasTopNotitificationInPage { - previousAccessiblityElement = getFirstFocusedElementOnScreen() - } else { - let accessbilityElement = getUIElementBasedOn(id: accessibilityId) - post(notification: .screenChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) - accessibilityId = nil - } - }*/ // MARK: - Accessibility Handler operation events open func capturePreviousFocusElement() { @@ -210,30 +194,49 @@ struct AccessibilityHandlerBehaviorModel: PageBehaviorModelProtocol { class AccessibilityHandlerBehavior: PageVisibilityBehavior { + private var delegateObj: MVMCoreUIDelegateObject? + private var anyCancellable: Set = [] + required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { - let viewController = updateAccessibilityViews(delegateObject) - AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() ?? AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) + guard let controller = delegateObject?.moleculeDelegate as? UIViewController, + (AccessibilityHandler.shared()?.canPostAccessbilityNotification(for: controller) ?? true), + AccessibilityHandler.shared()?.accessibilityId == nil else { return } + if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { + AccessibilityHandler.shared()?.previousAccessiblityElement = AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen() + } else { + AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) + } + delegateObj = delegateObject } - private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) -> UIViewController? { - var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController] - var viewController: UIViewController? + ///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?) { + updateAccessibilityViews(delegateObject) //To track FAB & HAB elements on UI + guard let accessibilityElement = AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() else { return } + if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { + AccessibilityHandler.shared()?.previousAccessiblityElement = accessibilityElement + } else { + AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: accessibilityElement) + } + } + + private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { + var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { - var managerControllerViews = [Any?]() - managerControllerViews.append(managerController.navigationController) - managerControllerViews.append(managerController.tabs) - managerControllerViews.append(contentsOf: managerController.view.subviews) - managerController.view.accessibilityElements = managerControllerViews.compactMap { $0 } + accessibilityElements.append(managerController.navigationController) + accessibilityElements.append(managerController.tabs) + accessibilityElements.append(contentsOf: managerController.view.subviews) + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + managerController.view.accessibilityElements = accessibilityElements.compactMap { $0 } } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { accessibilityElements.append(controller.navigationController) accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - controller.view.accessibilityElements = accessibilityElements - viewController = controller + controller.view.accessibilityElements = accessibilityElements.compactMap { $0 } } - return viewController } } From c1392f425335c64c73c33af9ee9b780139872818 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Sat, 23 Sep 2023 17:34:49 +0530 Subject: [PATCH 13/34] added missing code --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index fde20909..987eca37 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -200,6 +200,7 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, (AccessibilityHandler.shared()?.canPostAccessbilityNotification(for: controller) ?? true), AccessibilityHandler.shared()?.accessibilityId == nil else { return } From 44d2aa630ec5efdff23301bec8b9993c76434426 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Mon, 25 Sep 2023 16:26:25 +0530 Subject: [PATCH 14/34] refactored PreDefinedFocusedelement func --- .../Accessibility/AccessibilityHandler.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 987eca37..0ed3348b 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -171,12 +171,17 @@ open class AccessibilityHandler { open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } func getPreDefinedFocusedElementIfAny() -> UIView? { - guard let accessibilityId, let models: [any Identifiable] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } - guard !models.isEmpty, - let model = (models.filter { ($0.id as? String) == accessibilityId }).first else { return nil } + guard let accessibilityId else { return nil } + var modelElement: MoleculeModelProtocol? + ((delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate)?.getRootMolecules().depthFirstTraverse(options: .leafNodesOnly, depth: 0) { index, model, stop in + if model.id == accessibilityId { + modelElement = model + stop = true + } + } return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in - guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel as? (any Identifiable), - (moleculeModel.id as? String) == (model.id as? String) else { + guard let modelElement, let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel, + moleculeModel.id == modelElement.id else { return false } return true From 9c111471a802c9d60fce62b0900e3e0f35b7de21 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Sat, 23 Sep 2023 17:32:36 +0530 Subject: [PATCH 15/34] added custom button rotor for molecular views --- .../Accessibility/AccessibilityHandler.swift | 55 +++++++++++++++++++ .../AccessibilityModelProtocol.swift | 18 ++++++ .../Protocols/MoleculeViewProtocol.swift | 4 +- MVMCoreUI/BaseClasses/Button.swift | 5 ++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 0ed3348b..83b0347a 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -201,9 +201,12 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { 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?) { updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, @@ -229,6 +232,58 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { } } + //MARK: - Private Methods + private func identifyAndPrepareForButtonRotor() { + let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? UIViewController) + var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] + var currentIndexPath: IndexPath? + rotorElements = (currentViewController as? MoleculeListTemplate)?.templateModel?.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements, nextPartialResult: { result, model, depth in + if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), let indexPath = (currentViewController as? MoleculeListTemplate)?.getIndexPath(for: listModel) { currentIndexPath = indexPath + } + var result = result + if (model.accessibilityTraits?.contains(.button) ?? false), let currentIndexPath { + result.append((model, currentIndexPath)) + } + return result + }) ?? [] + 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) }) + } + 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 } + } + + 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() } + if predicate.searchDirection == .next { + self.currentRotorIndex += 1 + if self.currentRotorIndex > accessibilityButtons.count { + self.currentRotorIndex = 1 + } + } else { + self.currentRotorIndex -= 1 + if self.currentRotorIndex <= 0 { + self.currentRotorIndex = accessibilityButtons.count + } + } + var rotorElement = accessibilityButtons[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 } as Any + } + UIAccessibility.post(notification: .layoutChanged, argument: rotorElement) + return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) + } + } + private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift index a7508e39..809334d8 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift @@ -12,6 +12,9 @@ import Foundation public protocol AccessibilityModelProtocol { var accessibilityIdentifier: String? { get set } + var accessibilityTraits: UIAccessibilityTraits? { get set } + var accessibilityText: String? { get set } + var accessibilityValue: String? { get set } } public extension AccessibilityModelProtocol { @@ -20,4 +23,19 @@ public extension AccessibilityModelProtocol { get { nil } set { } } + + var accessibilityTraits: UIAccessibilityTraits? { + get { nil } + set { } + } + + var accessibilityText: String? { + get { nil } + set { } + } + + var accessibilityValue: String? { + get { nil } + set { } + } } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index dbd6b2df..3a6bc8f6 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -107,9 +107,9 @@ public protocol MoleculeViewModelProtocol: UIView { var moleculeModel: MoleculeModelProtocol? { get } } -extension MoleculeViewModelProtocol { +public extension MoleculeViewModelProtocol { - var moleculeModel: MoleculeModelProtocol? { + public var moleculeModel: MoleculeModelProtocol? { get { nil } } } diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index 15ea1e03..7450b716 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -161,3 +161,8 @@ extension Button: AppleGuidelinesProtocol { Self.acceptablyOutsideBounds(point: point, bounds: bounds) } } + +extension Button: MoleculeViewModelProtocol { + + public var moleculeModel: MoleculeModelProtocol? { model } +} From dad947d7c8c61e6ced6c777ca0255c4b51c8d6ea Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Mon, 25 Sep 2023 16:20:11 +0530 Subject: [PATCH 16/34] updated identifying buttons on view for rotor --- .../Accessibility/AccessibilityHandler.swift | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 83b0347a..d609aecd 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -224,6 +224,7 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { ///https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) //To track FAB & HAB elements on UI + identifyAndPrepareForButtonRotor() guard let accessibilityElement = AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() else { return } if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { AccessibilityHandler.shared()?.previousAccessiblityElement = accessibilityElement @@ -234,24 +235,15 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { //MARK: - Private Methods private func identifyAndPrepareForButtonRotor() { - let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? UIViewController) - var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] - var currentIndexPath: IndexPath? - rotorElements = (currentViewController as? MoleculeListTemplate)?.templateModel?.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements, nextPartialResult: { result, model, depth in - if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), let indexPath = (currentViewController as? MoleculeListTemplate)?.getIndexPath(for: listModel) { currentIndexPath = indexPath - } - var result = result - if (model.accessibilityTraits?.contains(.button) ?? false), let currentIndexPath { - result.append((model, currentIndexPath)) - } - return result - }) ?? [] + 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) }) } - accessibilityButtons?.append(contentsOf: rotorElements) + 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)}) } @@ -259,6 +251,29 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { currentViewController?.navigationController?.accessibilityCustomRotors = [createRotorForButtons()].compactMap { $0 } } + private func getRotorButtonsBasedOn(template: ViewController?) -> [Any]? { + if let currentViewController = template as? MoleculeListTemplate, let templateModel = currentViewController.templateModel { //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 { + 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 + } else if let currentViewController = template as? MoleculeStackTemplate { //Stack templates + return currentViewController.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(.button) } + } + 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 @@ -277,7 +292,7 @@ class AccessibilityHandlerBehavior: PageVisibilityBehavior { var rotorElement = accessibilityButtons[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 } as Any + 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 } UIAccessibility.post(notification: .layoutChanged, argument: rotorElement) return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) From 00427f9e774ef5139132d35247fd4151a3f2e032 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Tue, 26 Sep 2023 00:15:20 +0530 Subject: [PATCH 17/34] 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 From 6b2e29ae626e170d85b798e291097dd08957f0cb Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 27 Sep 2023 12:18:58 +0530 Subject: [PATCH 18/34] refactored code --- .../Accessibility/AccessibilityHandler.swift | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index d25ab81e..af9c13a5 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -179,11 +179,12 @@ open class AccessibilityHandler { stop = true } } - return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in + return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol, stop: inout Bool) in guard let modelElement, let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel, moleculeModel.id == modelElement.id else { return false } + stop = true return true }.first } @@ -242,9 +243,9 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { private func identifyAndPrepareRotors() { var rotorElements: [UIAccessibilityCustomRotor] = [] let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? ViewController) - for element in RotorType.allCases { - if let elements = getTraitMappedElements(template: currentViewController, type: element), - let rotor = createRotor(elements, for: element) { + for type in RotorType.allCases { + if let elements = getTraitMappedElements(template: currentViewController, type: type), + let rotor = createRotor(elements, for: type) { rotorElements.append(rotor) } } @@ -289,23 +290,23 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { return result }) - let headerViewElements = currentViewController.tableView.tableHeaderView?.getMoleculeViews { - (subView: MoleculeViewProtocol) in subView.accessibilityTraits.contains(type.trait) + 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) + 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 + .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(type.trait) + return currentViewController.view?.getMoleculeViews { (subView: MoleculeViewProtocol, _) in + subView.accessibilityTraits.contains(type.trait) } } return nil @@ -329,10 +330,10 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { 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(type.trait) - }.filter { - ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id + rotorElement = tableView.cellForRow(at: element.indexPath)?.getMoleculeViews { (subView: MoleculeViewProtocol, stop: inout Bool) in + guard subView.accessibilityTraits.contains(type.trait), (subView as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id else { return false } + stop = true + return true }.first as Any } UIAccessibility.post(notification: .layoutChanged, argument: rotorElement) @@ -369,9 +370,17 @@ extension UIView { } } - func getMoleculeViews(excludedViews: [UIView]? = nil, filter: ((T) -> Bool)) -> [T] { - return getNestedSubviews(excludedViews: excludedViews).compactMap { - filter($0) ? $0 : nil + func getMoleculeViews(excludedViews: [UIView]? = nil, filter: ((T, inout Bool) -> Bool)) -> [T] { + var stop = false + var results: [T] = [] + for element: T in getNestedSubviews(excludedViews: excludedViews) { + if filter(element, &stop) { + results.append(element) + } + if stop { + break + } } + return results } } From 46fb714d3495331b375db1e35b6b8c23cef3ba97 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 27 Sep 2023 20:53:50 +0530 Subject: [PATCH 19/34] added heading level rotor --- .../Accessibility/AccessibilityHandler.swift | 108 +++++++++++------- .../Atomic/Atoms/Buttons/ButtonModel.swift | 3 + .../Atomic/Atoms/Views/Label/Label.swift | 7 ++ .../Protocols/MoleculeViewProtocol.swift | 2 +- MVMCoreUI/BaseClasses/Button.swift | 4 + 5 files changed, 81 insertions(+), 43 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index af9c13a5..aeba4970 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -197,18 +197,32 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { enum RotorType: String, CaseIterable { case button = "Buttons" + case header = "Header" var trait: UIAccessibilityTraits { switch self { case .button: return .button + case .header: + return .header } } + + func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? { + var accessibilityCustomRotor: UIAccessibilityCustomRotor? + switch self { + case .header: + accessibilityCustomRotor = UIAccessibilityCustomRotor(systemType: .heading, itemSearch: itemSearch) + default: + accessibilityCustomRotor = UIAccessibilityCustomRotor(name: rawValue, itemSearch: itemSearch) + } + return accessibilityCustomRotor + } } public var anyCancellable: Set = [] private var delegateObj: MVMCoreUIDelegateObject? - private var currentRotorIndex: Int = 0 + private var rotorIndexes: [RotorType: Int] = [:] required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } @@ -239,43 +253,66 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { } } - //MARK: - Private Methods + //MARK: - Accessibility Methods + + private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { + var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] + if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { + accessibilityElements.append(managerController.navigationController) + accessibilityElements.append(managerController.tabs) + accessibilityElements.append(contentsOf: managerController.view.subviews) + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + managerController.view.accessibilityElements = accessibilityElements.compactMap { $0 } + } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { + accessibilityElements.append(controller.navigationController) + accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + controller.view.accessibilityElements = accessibilityElements.compactMap { $0 } + } + } + + //MARK: - Rotor Methods private func identifyAndPrepareRotors() { var rotorElements: [UIAccessibilityCustomRotor] = [] let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? ViewController) for type in RotorType.allCases { - if let elements = getTraitMappedElements(template: currentViewController, type: type), - let rotor = createRotor(elements, for: type) { + if let elements = getTraitMappedElements(currentViewController, type: type), + let rotor = createRotor(elements, for: type) { rotorElements.append(rotor) } } currentViewController?.navigationController?.accessibilityCustomRotors = rotorElements } - private func getTraitMappedElements(template: ViewController?, type: RotorType) -> [Any]? { - var accessibilityElements: [Any?]? = template?.navigationItem.leftBarButtonItems ?? [] - accessibilityElements?.append(contentsOf: template?.navigationItem.rightBarButtonItems ?? []) + private func getTraitMappedElements(_ template: ViewController?, type: RotorType) -> [Any]? { + var accessibilityElements: [Any?] = [] + switch type { + case .button: + accessibilityElements.append(contentsOf: template?.navigationItem.leftBarButtonItems ?? []) + accessibilityElements.append(contentsOf: template?.navigationItem.rightBarButtonItems ?? []) + case .header: + accessibilityElements.append(template?.navigationItem.titleView) + } if let tabs = (template as? SubNavManagerController)?.tabs { - accessibilityElements?.append(contentsOf: tabs.subviews.filter { + accessibilityElements.append(contentsOf: tabs.subviews.filter { $0.accessibilityTraits.contains(type.trait) }) } if let rotorElements = getRotorElementsFrom(template: template, type: type) { - accessibilityElements?.append(contentsOf: rotorElements) + accessibilityElements.append(contentsOf: rotorElements) } if let tabBarHidden = (template as? TabPageModelProtocol)?.tabBarHidden, !tabBarHidden { - accessibilityElements?.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { + accessibilityElements.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { $0.accessibilityTraits.contains(type.trait) }) } - return accessibilityElements?.compactMap { $0 } + 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 - + let templateModel = currentViewController.templateModel, + let tableView = currentViewController.tableView { //List templates var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] var currentIndexPath: IndexPath? @@ -301,7 +338,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { let otherInteractiveElements = currentViewController.view?.getMoleculeViews(excludedViews: [tableView, tableView.tableFooterView, tableView.tableHeaderView] .compactMap { $0 }) { (subView: MoleculeViewProtocol, _) in subView.accessibilityTraits.contains(type.trait) - } as? [Any] ?? [] + } as? [Any] ?? [] return headerViewElements + otherInteractiveElements + (rotorElements as [Any]) + footerViewElements } else if let currentViewController = template as? MoleculeStackTemplate { //Stack templates @@ -313,22 +350,24 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { } 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 !elements.isEmpty else { return nil } + return type.getUIAccessibilityCustomRotor { [weak self] predicate in guard let self else { return UIAccessibilityCustomRotorItemResult() } + var rotorIndex = self.rotorIndexes[type] ?? 0 if predicate.searchDirection == .next { - self.currentRotorIndex += 1 - if self.currentRotorIndex > elements.count { - self.currentRotorIndex = 1 + rotorIndex += 1 + if rotorIndex > elements.count { + rotorIndex = 1 } } else { - self.currentRotorIndex -= 1 - if self.currentRotorIndex <= 0 { - self.currentRotorIndex = elements.count + rotorIndex -= 1 + if rotorIndex <= 0 { + rotorIndex = elements.count } } - var rotorElement = elements[self.currentRotorIndex - 1] - if let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { + var rotorElement = elements[rotorIndex - 1] + if let tableView = (self.delegateObj?.moleculeListDelegate as? MoleculeListTemplate)?.tableView, + let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { //for List templates tableView.scrollToRow(at: element.indexPath, at: .middle, animated: false) rotorElement = tableView.cellForRow(at: element.indexPath)?.getMoleculeViews { (subView: MoleculeViewProtocol, stop: inout Bool) in guard subView.accessibilityTraits.contains(type.trait), (subView as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id else { return false } @@ -336,26 +375,11 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { return true }.first as Any } - UIAccessibility.post(notification: .layoutChanged, argument: rotorElement) + self.rotorIndexes[type] = rotorIndex + AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: rotorElement) return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) } } - - private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { - var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] - if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { - accessibilityElements.append(managerController.navigationController) - accessibilityElements.append(managerController.tabs) - accessibilityElements.append(contentsOf: managerController.view.subviews) - accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - managerController.view.accessibilityElements = accessibilityElements.compactMap { $0 } - } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { - accessibilityElements.append(controller.navigationController) - accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) - accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - controller.view.accessibilityElements = accessibilityElements.compactMap { $0 } - } - } } // MARK: - Helpers diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 08afefde..92b50d29 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -35,6 +35,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat public var size: Styler.Button.Size? = .standard public var groupName: String = "" public var inverted: Bool = false + public var accessibilityTraits: UIAccessibilityTraits? public lazy var enabledColors: FacadeElements = (fill: enabled_fillColor(), text: enabled_textColor(), @@ -195,6 +196,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat case disabledTextColor case disabledBorderColor case width + case accessibilityTraits } //-------------------------------------------------- @@ -207,6 +209,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) + accessibilityTraits = try typeContainer.decodeIfPresent(UIAccessibilityTraits.self, forKey: .accessibilityTraits) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 01614f31..119fc66b 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -44,6 +44,7 @@ public typealias ActionBlock = () -> () public var shouldMaskWhileRecording: Bool = false + private var model: MoleculeModelProtocol? //------------------------------------------------------ // MARK: - Multi-Action Text //------------------------------------------------------ @@ -408,6 +409,7 @@ public typealias ActionBlock = () -> () attributedText = attributedString originalAttributedString = attributedText } + self.model = labelModel } @objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { @@ -1027,3 +1029,8 @@ func validateAttribute(range: NSRange, in string: NSAttributedString, type: Stri return range } + +extension Label: MoleculeViewModelProtocol { + + public var moleculeModel: MoleculeModelProtocol? { model } +} diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 3a6bc8f6..1d7ebd03 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -109,7 +109,7 @@ public protocol MoleculeViewModelProtocol: UIView { public extension MoleculeViewModelProtocol { - public var moleculeModel: MoleculeModelProtocol? { + var moleculeModel: MoleculeModelProtocol? { get { nil } } } diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index 7450b716..c744452e 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -107,6 +107,10 @@ public typealias ButtonAction = (Button) -> () isEnabled = model.enabled } + if let accessibilityTraits = model.accessibilityTraits { + self.accessibilityTraits = accessibilityTraits + } + guard let model = model as? ButtonModelProtocol else { return } set(with: model.action, delegateObject: delegateObject, additionalData: additionalData) From 88505e704cab9b6ef93283311d34ad1e0597ffb1 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 4 Oct 2023 20:08:46 +0530 Subject: [PATCH 20/34] addressed review comments --- .../Accessibility/AccessibilityHandler.swift | 136 +++++------------- .../Atomic/Atoms/Selectors/ToggleModel.swift | 1 - .../Utility/MVMCoreUIUtility+Extension.swift | 7 +- 3 files changed, 43 insertions(+), 101 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index aeba4970..c77137a6 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -15,7 +15,6 @@ public class AccessbilityOperation: MVMCoreOperation { private let argument: Any? private let notificationType: UIAccessibility.Notification - private var timerSource: DispatchSourceTimer? public init(notificationType: UIAccessibility.Notification, argument: Any?) { self.notificationType = notificationType @@ -27,24 +26,18 @@ public class AccessbilityOperation: MVMCoreOperation { stop() return } - timerSource = DispatchSource.makeTimerSource() - timerSource?.setEventHandler { - Task { @MainActor [weak self] in - guard let self = self, !self.isCancelled else { - self?.stop() - return - } - UIAccessibility.post(notification: self.notificationType, argument: self.argument) - self.markAsFinished() + Task { @MainActor [weak self] in + guard let self = self, !self.isCancelled else { + self?.stop() + return } + UIAccessibility.post(notification: self.notificationType, argument: self.argument) + self.markAsFinished() } - timerSource?.schedule(deadline: .now()) - timerSource?.activate() } public func stop() { guard isCancelled else { return } - timerSource?.cancel() markAsFinished() } } @@ -65,26 +58,15 @@ open class AccessibilityHandler { queue.maxConcurrentOperationCount = 1 return queue }() - private(set) var accessibilityId: String? - private var announcementText: String? - private(set) var hasTopNotitificationInPage: Bool = false + public var accessibilityId: String? + public var hasTopNotificationInPage: Bool = false public init() { - registerWithResponseLoaded() registerForPageChanges() registerForFocusChanges() } // MARK: - Register with Accessibility Handler listeners - /// Registers with the notification center to know when json is updated and to capture previous accessbility focused id & announcment text - private func registerWithResponseLoaded() { - NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) - .sink { [weak self] notification in - self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") - self?.announcementText = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("announcementText") - }.store(in: &anyCancellable) - } - private func registerForFocusChanges() { //Since foucs shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) @@ -96,12 +78,14 @@ open class AccessibilityHandler { func registerForTopNotificationsChanges() { NotificationHandler.shared()?.onNotificationWillShow .sink { [weak self] (_, model) in - self?.hasTopNotitificationInPage = true - self?.capturePreviousFocusElement() + if self?.previousAccessiblityElement == nil { + self?.capturePreviousFocusElement() + } }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationShown .sink { [weak self] (view, model) in self?.post(notification: .layoutChanged, argument: view) + self?.hasTopNotificationInPage = false }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationWillDismiss .sink { [weak self] (view, model) in @@ -129,10 +113,7 @@ open class AccessibilityHandler { private func willNavigate(_ operation: NavigationOperation) { previousAccessiblityElement = nil - if let announcementText { - post(notification: .announcement, argument: announcementText) - } - if let subNavManagerController = operation.toNavigationControllerViewControllers?.last as? SubNavManagerController { + if let subNavManagerController = (operation.toNavigationControllerViewControllers?.last as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { delegate = subNavManagerController.getCurrentViewController() as? MVMCoreViewControllerProtocol } else { delegate = operation.toNavigationControllerViewControllers?.last as? MVMCoreViewControllerProtocol @@ -164,35 +145,23 @@ open class AccessibilityHandler { //To get first focus element on the screen open func getFirstFocusedElementOnScreen() -> Any? { - (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar + (delegate as? UIViewController)?.navigationController?.navigationBar } //Subclass can decide to trigger Accessibility notification on screen change. open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } func getPreDefinedFocusedElementIfAny() -> UIView? { - guard let accessibilityId else { return nil } - var modelElement: MoleculeModelProtocol? - ((delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate)?.getRootMolecules().depthFirstTraverse(options: .leafNodesOnly, depth: 0) { index, model, stop in - if model.id == accessibilityId { - modelElement = model - stop = true - } + guard let accessibilityId, let view = (delegate as? UIViewController)?.view else { return nil } + return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first { + ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == accessibilityId } - return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol, stop: inout Bool) in - guard let modelElement, let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel, - moleculeModel.id == modelElement.id else { - return false - } - stop = true - return true - }.first } } // 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. -open class AccessibilityHandlerBehavior: PageVisibilityBehavior { +open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior { enum RotorType: String, CaseIterable { @@ -226,13 +195,23 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return } + AccessibilityHandler.shared()?.accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") + //TODO: - Need to revisit this logic + AccessibilityHandler.shared()?.hasTopNotificationInPage = loadObject?.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || loadObject?.responseInfoMap?.optionalStringForKey("userMessage") != nil + if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") { + AccessibilityHandler.shared()?.post(notification: .announcement, argument: announcementText) + } + } + //MARK: - PageVisibiltyBehaviour open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, (AccessibilityHandler.shared()?.canPostAccessbilityNotification(for: controller) ?? true), AccessibilityHandler.shared()?.accessibilityId == nil else { return } - if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { + if AccessibilityHandler.shared()?.hasTopNotificationInPage ?? false { AccessibilityHandler.shared()?.previousAccessiblityElement = AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen() } else { AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) @@ -246,7 +225,8 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { updateAccessibilityViews(delegateObject) //To track FAB & HAB elements on UI identifyAndPrepareRotors() guard let accessibilityElement = AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() else { return } - if AccessibilityHandler.shared()?.hasTopNotitificationInPage ?? false { + AccessibilityHandler.shared()?.accessibilityId = nil + if AccessibilityHandler.shared()?.hasTopNotificationInPage ?? false { AccessibilityHandler.shared()?.previousAccessiblityElement = accessibilityElement } else { AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: accessibilityElement) @@ -254,8 +234,8 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { } //MARK: - Accessibility Methods - private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { + //TODO: - Need to revisit this logic var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { accessibilityElements.append(managerController.navigationController) @@ -313,7 +293,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { if let currentViewController = template as? MoleculeListTemplate, let templateModel = currentViewController.templateModel, let tableView = currentViewController.tableView { //List templates - var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] + var rotorElements: [(model: MoleculeModelProtocol, indexPath: IndexPath)] = [] ///Identifying the trait mapped elements models var currentIndexPath: IndexPath? rotorElements = templateModel.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements, nextPartialResult: { result, model, depth in @@ -327,24 +307,15 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { return result }) - let headerViewElements = currentViewController.tableView.tableHeaderView?.getMoleculeViews { (subView: MoleculeViewProtocol, _) in - subView.accessibilityTraits.contains(type.trait) - } as? [Any] ?? [] + let headerViewElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.tableView.tableHeaderView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - let footerViewElements = currentViewController.tableView.tableFooterView?.getMoleculeViews { (subView: MoleculeViewProtocol, _) in - subView.accessibilityTraits.contains(type.trait) - } as? [Any] ?? [] + let footerViewElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.tableView.tableFooterView].compactMap { $0 }).filter { $0.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] ?? [] + let otherInteractiveElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.view].compactMap { $0 }, excludedViews: [tableView, tableView.tableFooterView, tableView.tableHeaderView].compactMap { $0 }).filter { $0.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(type.trait) - } + return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] } return nil } @@ -369,11 +340,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { if let tableView = (self.delegateObj?.moleculeListDelegate as? MoleculeListTemplate)?.tableView, let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { //for List templates tableView.scrollToRow(at: element.indexPath, at: .middle, animated: false) - rotorElement = tableView.cellForRow(at: element.indexPath)?.getMoleculeViews { (subView: MoleculeViewProtocol, stop: inout Bool) in - guard subView.accessibilityTraits.contains(type.trait), (subView as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id else { return false } - stop = true - return true - }.first as Any + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [tableView.cellForRow(at: element.indexPath)].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) && ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id }.first as Any } self.rotorIndexes[type] = rotorIndex AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: rotorElement) @@ -381,30 +348,3 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior { } } } - -// MARK: - Helpers -extension UIView { - - 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(excludedViews: [UIView]? = nil, filter: ((T, inout Bool) -> Bool)) -> [T] { - var stop = false - var results: [T] = [] - for element: T in getNestedSubviews(excludedViews: excludedViews) { - if filter(element, &stop) { - results.append(element) - } - if stop { - break - } - } - return results - } -} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index fc267cff..008c2911 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -151,6 +151,5 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Identifiable try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) try container.encode(readOnly, forKey: .readOnly) - try container.encodeIfPresent(id, forKey: .id) } } diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift index e04de6bb..16677c7f 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift +++ b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift @@ -39,7 +39,7 @@ public extension MVMCoreUIUtility { /// - type: The type you are looking for. /// - views: The starting array of subviews. /// - Returns: Will return an array of any view associated with the given type. Will return an empty array of none were found. - static func findViews(by type: T.Type, views: [UIView]) -> [T] { + static func findViews(by type: T.Type, views: [UIView], excludedViews: [UIView] = []) -> [T] { guard !views.isEmpty else { return [] } @@ -47,6 +47,9 @@ public extension MVMCoreUIUtility { var matching = [T]() for view in views { + guard !excludedViews.contains(view) else { + continue + } if view is T { matching.append(view as! T) } @@ -54,7 +57,7 @@ public extension MVMCoreUIUtility { queue.append(contentsOf: view.subviews) } - return findViews(by: type, views: queue) + matching + return findViews(by: type, views: queue, excludedViews: excludedViews) + matching } static func visibleNavigationBarStlye() -> NavigationItemStyle? { From 8dc475ffdd7ca2cd1fbb6c019eb403b12befcd36 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 11 Oct 2023 00:47:31 +0530 Subject: [PATCH 21/34] addressed review comments & added model property to MoleculeViewProtocol --- .../Accessibility/AccessibilityHandler.swift | 322 +++++++++--------- MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift | 5 - .../Atomic/Atoms/Views/Label/Label.swift | 7 +- .../Atomic/Atoms/Views/ProgressBar.swift | 7 +- MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift | 8 +- .../HorizontalCombinationViews/TabBar.swift | 16 +- .../Protocols/MoleculeViewProtocol.swift | 19 +- MVMCoreUI/BaseClasses/Button.swift | 5 - .../NavigationController.swift | 5 + ...MCoreUISplitViewController+Extension.swift | 5 + .../SubNav/SubNavManagerController.swift | 4 + 11 files changed, 205 insertions(+), 198 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index c77137a6..f2bfd397 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -32,7 +32,13 @@ public class AccessbilityOperation: MVMCoreOperation { return } UIAccessibility.post(notification: self.notificationType, argument: self.argument) - self.markAsFinished() + if self.notificationType == .announcement { + NotificationCenter.default.addObserver(forName: UIAccessibility.announcementDidFinishNotification, object: nil, queue: .main) { _ in + self.markAsFinished() + } + } else { + self.markAsFinished() + } } } @@ -44,125 +50,6 @@ public class AccessbilityOperation: MVMCoreOperation { open class AccessibilityHandler { - public static func shared() -> Self? { - guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } - return MVMCoreActionUtility.fatalClassCheck(object: shared) - } - - public var previousAccessiblityElement: Any? - public var anyCancellable: Set = [] - public weak var delegate: MVMCoreViewControllerProtocol? - - private var accessibilityOperationQueue: OperationQueue = { - let queue = OperationQueue() - queue.maxConcurrentOperationCount = 1 - return queue - }() - public var accessibilityId: String? - public var hasTopNotificationInPage: Bool = false - - public init() { - registerForPageChanges() - registerForFocusChanges() - } - - // MARK: - Register with Accessibility Handler listeners - private func registerForFocusChanges() { - //Since foucs shifted to other elements cancelling existing focus shift notifications if any - NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) - .sink { [weak self] _ in - self?.cancelAllOperations() - }.store(in: &anyCancellable) - } - - func registerForTopNotificationsChanges() { - NotificationHandler.shared()?.onNotificationWillShow - .sink { [weak self] (_, model) in - if self?.previousAccessiblityElement == nil { - self?.capturePreviousFocusElement() - } - }.store(in: &anyCancellable) - NotificationHandler.shared()?.onNotificationShown - .sink { [weak self] (view, model) in - self?.post(notification: .layoutChanged, argument: view) - self?.hasTopNotificationInPage = false - }.store(in: &anyCancellable) - NotificationHandler.shared()?.onNotificationWillDismiss - .sink { [weak self] (view, model) in - self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) - }.store(in: &anyCancellable) - NotificationHandler.shared()?.onNotificationDismissed - .sink { [weak self] (view, model) in - self?.postAccessbilityToPrevElement() - }.store(in: &anyCancellable) - } - - /// Registers to know when pages change. - open func registerForPageChanges() { - NavigationHandler.shared() - .onNavigation - .sink { [self] (event, operation) in - switch event { - case .willNavigate: - willNavigate(operation) - default: - break - } - }.store(in: &anyCancellable) - } - - private func willNavigate(_ operation: NavigationOperation) { - previousAccessiblityElement = nil - if let subNavManagerController = (operation.toNavigationControllerViewControllers?.last as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { - delegate = subNavManagerController.getCurrentViewController() as? MVMCoreViewControllerProtocol - } else { - delegate = operation.toNavigationControllerViewControllers?.last as? MVMCoreViewControllerProtocol - } - } - - // MARK: - Accessibility Handler operation events - open func capturePreviousFocusElement() { - previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) - } - - open func postAccessbilityToPrevElement() { - post(notification: .layoutChanged, argument: previousAccessiblityElement) - } - - private func add(operation: Operation) { - accessibilityOperationQueue.addOperation(operation) - } - - private func cancelAllOperations() { - accessibilityOperationQueue.cancelAllOperations() - } - - public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { - guard UIAccessibility.isVoiceOverRunning else { return } - let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) - add(operation: accessbilityOperation) - } - - //To get first focus element on the screen - open func getFirstFocusedElementOnScreen() -> Any? { - (delegate as? UIViewController)?.navigationController?.navigationBar - } - - //Subclass can decide to trigger Accessibility notification on screen change. - open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } - - func getPreDefinedFocusedElementIfAny() -> UIView? { - guard let accessibilityId, let view = (delegate as? UIViewController)?.view else { return nil } - return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first { - ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == accessibilityId - } - } -} - -// 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. -open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior { - enum RotorType: String, CaseIterable { case button = "Buttons" @@ -189,72 +76,121 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra } } - public var anyCancellable: Set = [] - private var delegateObj: MVMCoreUIDelegateObject? - private var rotorIndexes: [RotorType: Int] = [:] + public static func shared() -> Self? { + guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } + return MVMCoreActionUtility.fatalClassCheck(object: shared) + } - required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } + public var accessibilityId: String? + public var previousAccessiblityElement: Any? + public var anyCancellable: Set = [] + public weak var delegate: MVMCoreViewControllerProtocol? + private var rotorIndexes: [RotorType: Int] = [:] + private var hasTopNotificationInPage: Bool { NotificationHandler.shared()?.isNotificationShowing() ?? false } + private let accessibilityOperationQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + + public init() { + registerForFocusChanges() + } + + // MARK: - Accessibility Handler operation events + open func capturePreviousFocusElement() { + previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) + } + + open func postAccessbilityToPrevElement() { + post(notification: .layoutChanged, argument: previousAccessiblityElement) + previousAccessiblityElement = nil + } + + public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { + guard UIAccessibility.isVoiceOverRunning else { return } + let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) + accessibilityOperationQueue.addOperation(accessbilityOperation) + } + + //To get first focus element on the screen + open func getFirstFocusedElementOnScreen() -> Any? { + (delegate as? UIViewController)?.navigationController?.navigationBar + } + + //Subclass can decide to trigger Accessibility notification on screen change. + open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } + + func getPreDefinedFocusedElementIfAny() -> UIView? { + guard let accessibilityId, let view = (delegate as? UIViewController)?.view else { return nil } + return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first { + $0.model?.id == accessibilityId + } + } +} + +extension AccessibilityHandler { public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + rotorIndexes = [:] + previousAccessiblityElement = nil guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return } - AccessibilityHandler.shared()?.accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") - //TODO: - Need to revisit this logic - AccessibilityHandler.shared()?.hasTopNotificationInPage = loadObject?.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || loadObject?.responseInfoMap?.optionalStringForKey("userMessage") != nil + accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") { - AccessibilityHandler.shared()?.post(notification: .announcement, argument: announcementText) + post(notification: .announcement, argument: announcementText) } + delegate = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } //MARK: - PageVisibiltyBehaviour - open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, - (AccessibilityHandler.shared()?.canPostAccessbilityNotification(for: controller) ?? true), - AccessibilityHandler.shared()?.accessibilityId == nil else { return } - if AccessibilityHandler.shared()?.hasTopNotificationInPage ?? false { - AccessibilityHandler.shared()?.previousAccessiblityElement = AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen() + canPostAccessbilityNotification(for: controller), + accessibilityId == nil else { return } + if hasTopNotificationInPage { + previousAccessiblityElement = getFirstFocusedElementOnScreen() } else { - AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) + post(notification: .layoutChanged, argument: getFirstFocusedElementOnScreen()) } - delegateObj = delegateObject } ///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 - open func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { - updateAccessibilityViews(delegateObject) //To track FAB & HAB elements on UI - identifyAndPrepareRotors() - guard let accessibilityElement = AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() else { return } - AccessibilityHandler.shared()?.accessibilityId = nil - if AccessibilityHandler.shared()?.hasTopNotificationInPage ?? false { - AccessibilityHandler.shared()?.previousAccessiblityElement = accessibilityElement + public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + identifyAndPrepareRotors(delegateObject) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + (delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil + } + guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return } + accessibilityId = nil + if hasTopNotificationInPage { + previousAccessiblityElement = accessibilityElement } else { - AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: accessibilityElement) + post(notification: .layoutChanged, argument: accessibilityElement) } } //MARK: - Accessibility Methods private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { - //TODO: - Need to revisit this logic - var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView] - if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { - accessibilityElements.append(managerController.navigationController) - accessibilityElements.append(managerController.tabs) - accessibilityElements.append(contentsOf: managerController.view.subviews) - accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - managerController.view.accessibilityElements = accessibilityElements.compactMap { $0 } - } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { - accessibilityElements.append(controller.navigationController) - accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) - accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - controller.view.accessibilityElements = accessibilityElements.compactMap { $0 } + var currentController = delegateObject?.moleculeDelegate as? UIViewController + var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController] + if let manager = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? MVMCoreViewManagerProtocol & UIViewController), + let managerAccessibilityElements = manager.getAccessibilityElements() { + accessibilityElements.append(contentsOf: managerAccessibilityElements) + accessibilityElements.append(contentsOf: manager.view.subviews) + currentController = manager + } else { + accessibilityElements.append(contentsOf: currentController?.view.subviews ?? []) } + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + currentController?.view.accessibilityElements = accessibilityElements.compactMap { $0 } } - + //MARK: - Rotor Methods - private func identifyAndPrepareRotors() { + private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) { var rotorElements: [UIAccessibilityCustomRotor] = [] - let currentViewController = ((delegateObj?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObj?.moleculeDelegate as? ViewController) + let currentViewController = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObject?.moleculeDelegate as? ViewController) for type in RotorType.allCases { if let elements = getTraitMappedElements(currentViewController, type: type), let rotor = createRotor(elements, for: type) { @@ -337,14 +273,76 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra } } var rotorElement = elements[rotorIndex - 1] - if let tableView = (self.delegateObj?.moleculeListDelegate as? MoleculeListTemplate)?.tableView, + if let tableView = (self.delegate as? MoleculeListTemplate)?.tableView, let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { //for List templates tableView.scrollToRow(at: element.indexPath, at: .middle, animated: false) - rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [tableView.cellForRow(at: element.indexPath)].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) && ($0 as? MoleculeViewModelProtocol)?.moleculeModel?.id == element.model.id }.first as Any + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [tableView.cellForRow(at: element.indexPath)].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) && $0.model?.id == element.model.id }.first as Any } self.rotorIndexes[type] = rotorIndex - AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: rotorElement) + post(notification: .layoutChanged, argument: rotorElement) return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) } } } + +@objc extension AccessibilityHandler { + + // MARK: - Register with Accessibility Handler listeners + private func registerForFocusChanges() { + //Since focus shifted to other elements cancelling existing focus shift notifications if any + NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) + .sink { [weak self] _ in + self?.accessibilityOperationQueue.cancelAllOperations() + }.store(in: &anyCancellable) + } + + func registerForTopNotificationsChanges() { + NotificationHandler.shared()?.onNotificationWillShow + .sink { [weak self] (_, model) in + if self?.previousAccessiblityElement == nil { + self?.capturePreviousFocusElement() + } + }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationShown + .sink { [weak self] (view, model) in + self?.post(notification: .layoutChanged, argument: view) + }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationWillDismiss + .sink { [weak self] (view, model) in + self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed"), priority: .veryHigh) + }.store(in: &anyCancellable) + NotificationHandler.shared()?.onNotificationDismissed + .sink { [weak self] (view, model) in + self?.postAccessbilityToPrevElement() + }.store(in: &anyCancellable) + } +} + +// 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. +open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior { + + public let accessibilityHandler: AccessibilityHandler? + + public init(accessibilityHandler: AccessibilityHandler?) { + self.accessibilityHandler = accessibilityHandler + } + + required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { + accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method. + } + + //MARK: - PageMoleculeTransformationBehavior + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject) + } + + //MARK: - PageVisibiltyBehaviour + open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + accessibilityHandler?.willShowPage(delegateObject) + } + + open func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + accessibilityHandler?.onPageShown(delegateObject) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index d84efad6..4341614e 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -418,8 +418,3 @@ extension Toggle { public func horizontalAlignment() -> UIStackView.Alignment { .trailing } } - -extension Toggle: MoleculeViewModelProtocol { - - public var moleculeModel: MoleculeModelProtocol? { model } -} diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 119fc66b..9af907e1 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -44,7 +44,7 @@ public typealias ActionBlock = () -> () public var shouldMaskWhileRecording: Bool = false - private var model: MoleculeModelProtocol? + public var model: MoleculeModelProtocol? //------------------------------------------------------ // MARK: - Multi-Action Text //------------------------------------------------------ @@ -1029,8 +1029,3 @@ func validateAttribute(range: NSRange, in string: NSAttributedString, type: Stri return range } - -extension Label: MoleculeViewModelProtocol { - - public var moleculeModel: MoleculeModelProtocol? { model } -} diff --git a/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift index a9fde5c1..a68cdc99 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift @@ -13,8 +13,11 @@ import Foundation //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - - var progressBarModel: ProgressBarModel? + public var model: MoleculeModelProtocol? + public var progressBarModel: ProgressBarModel? { + get { model as? ProgressBarModel } + set { model = newValue } + } var thickness: CGFloat = 8.0 { willSet(newValue) { diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift index 41d68493..55d83e75 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift @@ -18,7 +18,13 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{ //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var viewModel: TileletModel! + + public var model: MoleculeModelProtocol? + + public var viewModel: TileletModel! { + get { model as? TileletModel } + set { model = newValue } + } public var delegateObject: MVMCoreUIDelegateObject? public var additionalData: [AnyHashable: Any]? diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift index e49006fa..ebc7f469 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift @@ -9,7 +9,13 @@ import VDSColorTokens @objcMembers open class TabBar: UITabBar, MoleculeViewProtocol, TabBarProtocol, UITabBarDelegate { - public var model: TabBarModel + public var model: MoleculeModelProtocol? + + public var tabModel: TabBarModel { + get { model as! TabBarModel } + set { model = newValue } + } + public var delegateObject: MVMCoreUIDelegateObject? public let line = Line() @@ -68,8 +74,8 @@ import VDSColorTokens // MARK: - UITabBarDelegate public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - model.selectedTab = item.tag - let action = model.tabs[item.tag].action + tabModel.selectedTab = item.tag + let action = tabModel.tabs[item.tag].action Task(priority: .userInitiated) { try await Button.performButtonAction(with: action, button: item, delegateObject: delegateObject, additionalData: nil) } @@ -79,7 +85,7 @@ import VDSColorTokens public func highlightTab(at index: Int) { MVMCoreDispatchUtility.performBlock(onMainThread: { guard let newSelectedItem = self.items?[index] else { return } - self.model.selectedTab = index + self.tabModel.selectedTab = index self.selectedItem = newSelectedItem }) } @@ -92,7 +98,7 @@ import VDSColorTokens }) } - public func currentTabIndex() -> Int { model.selectedTab } + public func currentTabIndex() -> Int { tabModel.selectedTab } } extension UITabBarItem: MFButtonProtocol { } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 1d7ebd03..7dd4fa26 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -12,6 +12,8 @@ import MVMCore.MVMCoreViewProtocol public protocol MoleculeViewProtocol: UIView, ModelHandlerProtocol { + var model: MoleculeModelProtocol? { get set } + /// Initializes the view with the model init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) @@ -33,6 +35,11 @@ public protocol MoleculeViewProtocol: UIView, ModelHandlerProtocol { extension MoleculeViewProtocol { + public var model: MoleculeModelProtocol? { + get { nil } + set { } + } + /// Calls set with model public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { self.init(frame: .zero) @@ -101,15 +108,3 @@ public extension ModelRegistry { } } } - -public protocol MoleculeViewModelProtocol: UIView { - - var moleculeModel: MoleculeModelProtocol? { get } -} - -public extension MoleculeViewModelProtocol { - - var moleculeModel: MoleculeModelProtocol? { - get { nil } - } -} diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index c744452e..f4c7c418 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -165,8 +165,3 @@ extension Button: AppleGuidelinesProtocol { Self.acceptablyOutsideBounds(point: point, bounds: bounds) } } - -extension Button: MoleculeViewModelProtocol { - - public var moleculeModel: MoleculeModelProtocol? { model } -} diff --git a/MVMCoreUI/Containers/NavigationController/NavigationController.swift b/MVMCoreUI/Containers/NavigationController/NavigationController.swift index 8f72a792..5c7a9757 100644 --- a/MVMCoreUI/Containers/NavigationController/NavigationController.swift +++ b/MVMCoreUI/Containers/NavigationController/NavigationController.swift @@ -83,6 +83,11 @@ import Combine } extension NavigationController: MVMCoreViewManagerProtocol { + + public func getAccessibilityElements() -> [Any]? { + nil + } + public func getCurrentViewController() -> UIViewController? { guard let topViewController = topViewController else { return nil } return MVMCoreUIUtility.getViewControllerTraversingManagers(topViewController) diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift index 20f4449c..55e1071c 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift @@ -249,6 +249,11 @@ public extension MVMCoreUISplitViewController { } extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol { + + public func getAccessibilityElements() -> [Any]? { + nil + } + public func getCurrentViewController() -> UIViewController? { navigationController?.getCurrentViewController() } diff --git a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift index 458c0169..24c1ed23 100644 --- a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift +++ b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift @@ -307,6 +307,10 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, } } + @objc public func getAccessibilityElements() -> [Any]? { + [tabs] + } + open func newDataReceived(in viewController: UIViewController) { manager?.newDataReceived?(in: viewController) hideNavigationBarLine(true) From 521eaa7c159d002874b47b631f507edc933ccb3f Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 13 Oct 2023 22:28:00 +0530 Subject: [PATCH 22/34] Added RotorHandler & addressed review comments --- MVMCoreUI.xcodeproj/project.pbxproj | 6 +- .../Accessibility/AccessibilityHandler.swift | 162 ++----------- MVMCoreUI/Accessibility/RotorHandler.swift | 228 ++++++++++++++++++ MVMCoreUI/Alerts/AlertOperation.swift | 2 +- .../Templates/MoleculeListTemplate.swift | 4 +- .../ThreeLayerTableViewController.swift | 11 +- MVMCoreUI/OtherHandlers/CoreUIObject.swift | 23 +- MVMCoreUI/OtherHandlers/MVMCoreUISession.m | 4 +- 8 files changed, 283 insertions(+), 157 deletions(-) create mode 100644 MVMCoreUI/Accessibility/RotorHandler.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 4e04dafc..d1cb1f65 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -169,6 +169,7 @@ 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; }; + 71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; }; 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; }; 8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */; }; @@ -756,6 +757,7 @@ 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 = ""; }; 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; + 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotorHandler.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 = ""; }; 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextBodyTextModel.swift; sourceTree = ""; }; @@ -1457,6 +1459,7 @@ isa = PBXGroup; children = ( 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */, + 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */, ); path = Accessibility; sourceTree = ""; @@ -3012,6 +3015,7 @@ D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */, D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, + 71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */, AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */, 8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */, diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index f2bfd397..327681e8 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -49,49 +49,24 @@ public class AccessbilityOperation: MVMCoreOperation { } open class AccessibilityHandler { - - enum RotorType: String, CaseIterable { - case button = "Buttons" - case header = "Header" - - var trait: UIAccessibilityTraits { - switch self { - case .button: - return .button - case .header: - return .header - } - } - - func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? { - var accessibilityCustomRotor: UIAccessibilityCustomRotor? - switch self { - case .header: - accessibilityCustomRotor = UIAccessibilityCustomRotor(systemType: .heading, itemSearch: itemSearch) - default: - accessibilityCustomRotor = UIAccessibilityCustomRotor(name: rawValue, itemSearch: itemSearch) - } - return accessibilityCustomRotor - } - } - public static func shared() -> Self? { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } - public var accessibilityId: String? public var previousAccessiblityElement: Any? public var anyCancellable: Set = [] - public weak var delegate: MVMCoreViewControllerProtocol? - private var rotorIndexes: [RotorType: Int] = [:] - private var hasTopNotificationInPage: Bool { NotificationHandler.shared()?.isNotificationShowing() ?? false } + public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } + public weak var delegateObject: MVMCoreUIDelegateObject? + private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("userMessage") != nil } private let accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 return queue }() + + lazy var rotorHandler = RotorHandler(accessibilityHandler: self) public init() { registerForFocusChanges() @@ -129,17 +104,22 @@ open class AccessibilityHandler { } } +/** + When we push a new viewcontroller on to a Navigation stack from iOS 13+ Accessibility voiceover is going to the element inside of the viewcontroller. Not treating navigationController left/back bar button as first element. So alternatively we are setting accessibility elements in viewWillAppear untill viewDidAppear then we are resetting back So that there will not be accessibility order issue. + https://developer.apple.com/forums/thread/655359 + https://developer.apple.com/forums/thread/675427 + */ extension AccessibilityHandler { public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { - rotorIndexes = [:] previousAccessiblityElement = nil + rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject) guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return } accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") { post(notification: .announcement, argument: announcementText) } - delegate = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol + self.delegateObject = delegateObject } //MARK: - PageVisibiltyBehaviour @@ -158,10 +138,12 @@ extension AccessibilityHandler { ///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 public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { - identifyAndPrepareRotors(delegateObject) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - (delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil + defer { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + (delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil + } } + rotorHandler.onPageShown(delegateObject) guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return } accessibilityId = nil if hasTopNotificationInPage { @@ -173,115 +155,19 @@ extension AccessibilityHandler { //MARK: - Accessibility Methods private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { - var currentController = delegateObject?.moleculeDelegate as? UIViewController + guard var currentController = delegateObject?.moleculeDelegate as? UIViewController, + currentController.navigationController != nil else { return }///If no navigationController, we can go with the default behaviour of Accessibility voiceover. var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController] if let manager = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? MVMCoreViewManagerProtocol & UIViewController), - let managerAccessibilityElements = manager.getAccessibilityElements() { + let managerAccessibilityElements = manager.getAccessibilityElements() { accessibilityElements.append(contentsOf: managerAccessibilityElements) accessibilityElements.append(contentsOf: manager.view.subviews) currentController = manager } else { - accessibilityElements.append(contentsOf: currentController?.view.subviews ?? []) + accessibilityElements.append(contentsOf: currentController.view.subviews) } accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) - currentController?.view.accessibilityElements = accessibilityElements.compactMap { $0 } - } - - //MARK: - Rotor Methods - private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) { - var rotorElements: [UIAccessibilityCustomRotor] = [] - let currentViewController = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController) ?? (delegateObject?.moleculeDelegate as? ViewController) - for type in RotorType.allCases { - if let elements = getTraitMappedElements(currentViewController, type: type), - let rotor = createRotor(elements, for: type) { - rotorElements.append(rotor) - } - } - currentViewController?.navigationController?.accessibilityCustomRotors = rotorElements - } - - private func getTraitMappedElements(_ template: ViewController?, type: RotorType) -> [Any]? { - var accessibilityElements: [Any?] = [] - switch type { - case .button: - accessibilityElements.append(contentsOf: template?.navigationItem.leftBarButtonItems ?? []) - accessibilityElements.append(contentsOf: template?.navigationItem.rightBarButtonItems ?? []) - case .header: - accessibilityElements.append(template?.navigationItem.titleView) - } - 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)] = [] ///Identifying the trait mapped elements models - 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(type.trait) ?? false), let currentIndexPath { - result.append((model, currentIndexPath)) - } - return result - }) - - let headerViewElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.tableView.tableHeaderView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - - let footerViewElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.tableView.tableFooterView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - - let otherInteractiveElements = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.view].compactMap { $0 }, excludedViews: [tableView, tableView.tableFooterView, tableView.tableHeaderView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - - return headerViewElements + otherInteractiveElements + (rotorElements as [Any]) + footerViewElements - } else if let currentViewController = template as? MoleculeStackTemplate { //Stack templates - return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [currentViewController.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - } - return nil - } - - private func createRotor(_ elements: [Any], for type: RotorType) -> UIAccessibilityCustomRotor? { - guard !elements.isEmpty else { return nil } - return type.getUIAccessibilityCustomRotor { [weak self] predicate in - guard let self else { return UIAccessibilityCustomRotorItemResult() } - var rotorIndex = self.rotorIndexes[type] ?? 0 - if predicate.searchDirection == .next { - rotorIndex += 1 - if rotorIndex > elements.count { - rotorIndex = 1 - } - } else { - rotorIndex -= 1 - if rotorIndex <= 0 { - rotorIndex = elements.count - } - } - var rotorElement = elements[rotorIndex - 1] - if let tableView = (self.delegate as? MoleculeListTemplate)?.tableView, - let element = rotorElement as? (model: MoleculeModelProtocol, indexPath: IndexPath) { //for List templates - tableView.scrollToRow(at: element.indexPath, at: .middle, animated: false) - rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [tableView.cellForRow(at: element.indexPath)].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) && $0.model?.id == element.model.id }.first as Any - } - self.rotorIndexes[type] = rotorIndex - post(notification: .layoutChanged, argument: rotorElement) - return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) - } + currentController.view.accessibilityElements = accessibilityElements.compactMap { $0 } } } @@ -309,7 +195,7 @@ extension AccessibilityHandler { }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationWillDismiss .sink { [weak self] (view, model) in - self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed"), priority: .veryHigh) + self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationDismissed .sink { [weak self] (view, model) in @@ -333,7 +219,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra } //MARK: - PageMoleculeTransformationBehavior - public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject) } diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift new file mode 100644 index 00000000..98841cc0 --- /dev/null +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -0,0 +1,228 @@ +// +// RotorHandler.swift +// MVMCoreUI +// +// Created by Bandaru, Krishna Kishore on 13/10/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import Combine + +fileprivate enum RotorType: String, CaseIterable { + + case button = "Buttons" + case header = "Header" + + var trait: UIAccessibilityTraits { + switch self { + case .button: + return .button + case .header: + return .header + } + } + + func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? { + var accessibilityCustomRotor: UIAccessibilityCustomRotor? + switch self { + case .header: + accessibilityCustomRotor = UIAccessibilityCustomRotor(systemType: .heading, itemSearch: itemSearch) + default: + accessibilityCustomRotor = UIAccessibilityCustomRotor(name: rawValue, itemSearch: itemSearch) + } + return accessibilityCustomRotor + } +} + +public protocol RotorViewElementsProtocol: UIViewController { + var topView: UIView? { get set } + var middleView: UIView? { get set } + var bottomView: UIView? { get set } +} + +public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol { + func scrollRectToVisible(_ rect: CGRect, animated: Bool) +} + +public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol { + func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) -> Void + func cellForRow(at indexPath: IndexPath) -> UIView? +} + +struct RotorElement { + let indexPath: IndexPath + let model: MoleculeModelProtocol +} + +class RotorHandler { + + private var rotorIndexes: [RotorType: Int] = [:] + private var rotorElements: [RotorType: [Any]] = [:] + private var anyCancellable: Set = [] + private weak var delegateObject: MVMCoreUIDelegateObject? + private weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } + private weak var accessibilityHandler: AccessibilityHandler? + + init(accessibilityHandler: AccessibilityHandler?) { + self.accessibilityHandler = accessibilityHandler + registerForVoiceOverChanges() + } + + private func registerForVoiceOverChanges() { + NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) + .sink { [weak self] _ in + guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return } + self?.identifyAndPrepareRotors(self?.delegateObject) + }.store(in: &anyCancellable) + } + + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + rotorIndexes = [:] + rotorElements = [:] + self.delegateObject = delegateObject + } + + public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + identifyAndPrepareRotors(delegateObject) + } + + private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) { + guard UIAccessibility.isVoiceOverRunning, + let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return } + var customRotors: [UIAccessibilityCustomRotor] = [] + for type in RotorType.allCases { + if let elements = getTraitMappedElements(currentViewController, type: type), + !elements.isEmpty, + let rotor = createRotor(for: type) { + rotorElements[type] = elements + customRotors.append(rotor) + } + } + currentViewController.accessibilityCustomRotors = customRotors + } + + private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { + var accessibilityElements: [Any?] = [] + if let manager = (template as? MVMCoreViewManagerViewControllerProtocol)?.manager as? (MVMCoreViewControllerProtocol & UIViewController) { + accessibilityElements.append(contentsOf: getRotorElementsFrom(manager: manager, type: type) ?? []) + } + 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(manager: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { + guard let elements = (manager as? MVMCoreViewManagerProtocol)?.getAccessibilityElements() as? [UIView] else { return nil } + return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + } + + private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { + guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & UIViewController) else { + return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] //BAU Pages + } + let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + var reusableViewRotorElements: [Any] = [] + if let middleView = template.middleView { + if middleView.isKind(of: UITableView.self), + let template = template as? (any MoleculeListProtocol & ViewController) { + reusableViewRotorElements = getRotorElements(from: template, type: type) ?? [] + } else { + reusableViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [middleView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + } + } + let bottomViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.bottomView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.middleView, template.bottomView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + return topViewRotorElements + reusableViewRotorElements + remainingRotorElements + bottomViewRotorElements + } + + private func getRotorElements(from template: (any MoleculeListProtocol & ViewController)?, type: RotorType) -> [Any]? { + guard let templateModel = template?.model, + let moleculeList = template else { return nil } + var rotorElements: [RotorElement] = [] + var traitIndexPath: IndexPath? + rotorElements = templateModel.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements) { result, model, depth in + if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), let indexPath = moleculeList.getIndexPath(for: listModel) { + traitIndexPath = indexPath + } + var result = result + if (model.accessibilityTraits?.contains(type.trait) ?? false), let traitIndexPath { + result.append(.init(indexPath: traitIndexPath, model: model)) + } + return result + } + return rotorElements + } + + private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? { + return type.getUIAccessibilityCustomRotor { [weak self] predicate in + guard let self, let elements = self.rotorElements[type] else { return UIAccessibilityCustomRotorItemResult() } + var rotorIndex = self.rotorIndexes[type] ?? 0 + if predicate.searchDirection == .next { + rotorIndex += 1 + if rotorIndex > elements.count { + rotorIndex = 1 + } + } else { + rotorIndex -= 1 + if rotorIndex <= 0 { + rotorIndex = elements.count + } + } + var rotorElement = elements[rotorIndex - 1] + if let element = rotorElement as? RotorElement, + let controller = self.delegate as? RotorListTypeDelegateProtocol { + controller.scrollToRow(at: element.indexPath, at: .middle, animated: false) + guard let cellView = controller.cellForRow(at: element.indexPath) else { return UIAccessibilityCustomRotorItemResult() } + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView]).filter { $0.accessibilityTraits.contains(type.trait) && $0.model?.id == element.model.id }.first as Any + } else { + if let viewElement = (rotorElement as? UIView) { + let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view) + (self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false) + } + } + self.rotorIndexes[type] = rotorIndex + self.accessibilityHandler?.post(notification: .layoutChanged, argument: rotorElement) + return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil) + } + } +} + +public extension RotorViewElementsProtocol { + var topView: UIView? { nil } + var middleView: UIView? { nil } + var bottomView: UIView? { nil } +} + +extension RotorScrollDelegateProtocol { + public func scrollRectToVisible(_ rect: CGRect, animated: Bool) { } +} + +extension RotorListTypeDelegateProtocol { + func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { } + func cellForRow(at indexPath: IndexPath) -> UIView? { nil } +} + +extension ThreeLayerTableViewController: RotorListTypeDelegateProtocol { + + public func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { + tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: animated) + } + + public func cellForRow(at indexPath: IndexPath) -> UIView? { + tableView.cellForRow(at: indexPath) + } +} + +extension ThreeLayerViewController: RotorScrollDelegateProtocol { + + public func scrollRectToVisible(_ rect: CGRect, animated: Bool) { + scrollView?.scrollRectToVisible(rect, animated: animated) + } +} diff --git a/MVMCoreUI/Alerts/AlertOperation.swift b/MVMCoreUI/Alerts/AlertOperation.swift index 088a6ff5..50b71e46 100644 --- a/MVMCoreUI/Alerts/AlertOperation.swift +++ b/MVMCoreUI/Alerts/AlertOperation.swift @@ -73,7 +73,7 @@ public class AlertOperation: MVMCoreOperation { if await !self.properties.getIsDisplayed() { self.markAsFinished() } else { - (CoreUIObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject) + (MVMCoreObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject) if self.isCancelled { await self.dismissAlertView() } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 950b6785..a113ef5f 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -294,8 +294,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol let classMoleculeB = moleculeB as? NSObjectProtocol { return classMoleculeA === classMoleculeB } - // Do json check - return moleculeA.toJSON() == moleculeB.toJSON() + // ID check + return moleculeA.id == moleculeB.id } } diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index 4faff136..a6d6c7b5 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -8,15 +8,20 @@ import UIKit -open class ThreeLayerTableViewController: ProgrammaticTableViewController { +open class ThreeLayerTableViewController: ProgrammaticTableViewController, RotorViewElementsProtocol { + //-------------------------------------------------- // MARK: - Main Views //-------------------------------------------------- - private var topView: UIView? - private var bottomView: UIView? private var headerView: UIView? private var footerView: UIView? + public var topView: UIView? + public var middleView: UIView? { + get { tableView } + set { } + } + public var bottomView: UIView? //-------------------------------------------------- // MARK: - Properties diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index dd82a34f..131c2287 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -9,7 +9,12 @@ import UIKit import MVMCore -@objcMembers open class CoreUIObject: MVMCoreObject { +@objcMembers +public class CoreUIObject: NSObject { + private static var singleton = CoreUIObject() + public static func sharedInstance() -> CoreUIObject? { singleton } + private override init() {} + public var alertHandler: AlertHandler? public var topNotificationHandler: NotificationHandler? { didSet { @@ -17,16 +22,14 @@ import MVMCore } } public var accessibilityHandler: AccessibilityHandler? - - open override func defaultInitialSetup() { + + public func defaultInitialSetup() { + MVMCoreObject.sharedInstance()?.defaultInitialSetup() CoreUIModelMapping.registerObjects() - loadHandler = MVMCoreLoadHandler() - cache = MVMCoreCache() - session = MVMCoreUISession() - sessionHandler = MVMCoreSessionTimeHandler() - actionHandler = MVMCoreUIActionHandler() - viewControllerMapping = MVMCoreUIViewControllerMappingObject() - loggingDelegate = MVMCoreUILoggingHandler() + MVMCoreObject.sharedInstance()?.session = MVMCoreUISession() + MVMCoreObject.sharedInstance()?.actionHandler = MVMCoreUIActionHandler() + MVMCoreObject.sharedInstance()?.viewControllerMapping = MVMCoreUIViewControllerMappingObject() + MVMCoreObject.sharedInstance()?.loggingDelegate = MVMCoreUILoggingHandler() alertHandler = AlertHandler() accessibilityHandler = AccessibilityHandler() } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m index e1ca231f..3b1d5913 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -9,8 +9,8 @@ #import "MVMCoreUISession.h" #import "MFLoadingViewController.h" #import "NSLayoutConstraint+MFConvenience.h" -#import -@import MVMCore.MVMCoreObject; +@import MVMCore.MVMCoreLoadingOverlayDelegateProtocol; +@import MVMCore.Swift; @interface MVMCoreUISession () From 67dfe37e97dd9c0cfe25a2e70a0c3f84b0ec3d33 Mon Sep 17 00:00:00 2001 From: Keerthy Date: Mon, 16 Oct 2023 12:49:35 +0530 Subject: [PATCH 23/34] Addressed review comments and removed model based traits for button --- .../Accessibility/AccessibilityHandler.swift | 2 +- MVMCoreUI/Accessibility/RotorHandler.swift | 17 ++++++++--------- .../Atomic/Atoms/Buttons/ButtonModel.swift | 3 --- MVMCoreUI/BaseClasses/Button.swift | 4 ---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 327681e8..cefae3ea 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -59,7 +59,7 @@ open class AccessibilityHandler { public var anyCancellable: Set = [] public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } public weak var delegateObject: MVMCoreUIDelegateObject? - private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("userMessage") != nil } + private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil } private let accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 98841cc0..2d4b6e9c 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -125,7 +125,7 @@ class RotorHandler { private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & UIViewController) else { - return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] //BAU Pages + return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] } let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] var reusableViewRotorElements: [Any] = [] @@ -163,19 +163,18 @@ class RotorHandler { private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? { return type.getUIAccessibilityCustomRotor { [weak self] predicate in guard let self, let elements = self.rotorElements[type] else { return UIAccessibilityCustomRotorItemResult() } - var rotorIndex = self.rotorIndexes[type] ?? 0 + var rotorIndex = self.rotorIndexes[type] ?? -1 if predicate.searchDirection == .next { - rotorIndex += 1 - if rotorIndex > elements.count { - rotorIndex = 1 + if rotorIndex + 1 < elements.count { + rotorIndex += 1 } } else { - rotorIndex -= 1 - if rotorIndex <= 0 { - rotorIndex = elements.count + if rotorIndex > 0 { + rotorIndex -= 1 } } - var rotorElement = elements[rotorIndex - 1] + guard rotorIndex >= 0 else { return UIAccessibilityCustomRotorItemResult() } + var rotorElement = elements[rotorIndex] if let element = rotorElement as? RotorElement, let controller = self.delegate as? RotorListTypeDelegateProtocol { controller.scrollToRow(at: element.indexPath, at: .middle, animated: false) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 92b50d29..08afefde 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -35,7 +35,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat public var size: Styler.Button.Size? = .standard public var groupName: String = "" public var inverted: Bool = false - public var accessibilityTraits: UIAccessibilityTraits? public lazy var enabledColors: FacadeElements = (fill: enabled_fillColor(), text: enabled_textColor(), @@ -196,7 +195,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat case disabledTextColor case disabledBorderColor case width - case accessibilityTraits } //-------------------------------------------------- @@ -209,7 +207,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) - accessibilityTraits = try typeContainer.decodeIfPresent(UIAccessibilityTraits.self, forKey: .accessibilityTraits) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index f4c7c418..15ea1e03 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -107,10 +107,6 @@ public typealias ButtonAction = (Button) -> () isEnabled = model.enabled } - if let accessibilityTraits = model.accessibilityTraits { - self.accessibilityTraits = accessibilityTraits - } - guard let model = model as? ButtonModelProtocol else { return } set(with: model.action, delegateObject: delegateObject, additionalData: additionalData) From cb50234090656b9b6aa7380288bcef61cee6831e Mon Sep 17 00:00:00 2001 From: Keerthy Date: Mon, 16 Oct 2023 13:00:12 +0530 Subject: [PATCH 24/34] Spelling refactoring --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index cefae3ea..488ae403 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -11,7 +11,7 @@ import Combine import MVMCore import WebKit -public class AccessbilityOperation: MVMCoreOperation { +public class AccessibilityOperation: MVMCoreOperation { private let argument: Any? private let notificationType: UIAccessibility.Notification @@ -84,8 +84,8 @@ open class AccessibilityHandler { public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } - let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) - accessibilityOperationQueue.addOperation(accessbilityOperation) + let AccessibilityOperation = AccessibilityOperation(notificationType: type, argument: argument) + accessibilityOperationQueue.addOperation(AccessibilityOperation) } //To get first focus element on the screen From f73dbee1902a3c91c83aa6134382ef713b019a73 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Mon, 16 Oct 2023 18:48:49 +0530 Subject: [PATCH 25/34] Added rotor for checkbox --- MVMCoreUI/Accessibility/RotorHandler.swift | 45 +++++++++++++------ .../Atoms/Views/CheckboxLabelModel.swift | 3 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 2d4b6e9c..5819b198 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -13,6 +13,7 @@ fileprivate enum RotorType: String, CaseIterable { case button = "Buttons" case header = "Header" + case checkbox = "Checkbox" var trait: UIAccessibilityTraits { switch self { @@ -20,6 +21,26 @@ fileprivate enum RotorType: String, CaseIterable { return .button case .header: return .header + default: + return .none + } + } + + var modelFilter: ((MoleculeModelProtocol) -> Bool) { + switch self { + case .checkbox: + return { $0 is CheckboxModel } + default: + return { $0.accessibilityTraits?.contains(trait) ?? false } + } + } + + var filter: ((UIView) -> Bool) { + switch self { + case .checkbox: + return { $0 is Checkbox } + default: + return { $0.accessibilityTraits.contains(trait) } } } @@ -35,7 +56,7 @@ fileprivate enum RotorType: String, CaseIterable { } } -public protocol RotorViewElementsProtocol: UIViewController { +public protocol RotorViewElementsProtocol: MVMCoreViewControllerProtocol { var topView: UIView? { get set } var middleView: UIView? { get set } var bottomView: UIView? { get set } @@ -89,7 +110,7 @@ class RotorHandler { private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) { guard UIAccessibility.isVoiceOverRunning, - let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return } + let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return } var customRotors: [UIAccessibilityCustomRotor] = [] for type in RotorType.allCases { if let elements = getTraitMappedElements(currentViewController, type: type), @@ -111,34 +132,32 @@ class RotorHandler { 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) - }) + accessibilityElements.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { type.filter($0) }) } return accessibilityElements.compactMap { $0 } } private func getRotorElementsFrom(manager: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { guard let elements = (manager as? MVMCoreViewManagerProtocol)?.getAccessibilityElements() as? [UIView] else { return nil } - return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { type.filter($0) } as [Any] } private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & UIViewController) else { - return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] } - let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { type.filter($0) } as [Any] var reusableViewRotorElements: [Any] = [] if let middleView = template.middleView { if middleView.isKind(of: UITableView.self), let template = template as? (any MoleculeListProtocol & ViewController) { reusableViewRotorElements = getRotorElements(from: template, type: type) ?? [] } else { - reusableViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [middleView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + reusableViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [middleView].compactMap { $0 }).filter { type.filter($0) } as [Any] } } - let bottomViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.bottomView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] - let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.middleView, template.bottomView].compactMap { $0 }).filter { $0.accessibilityTraits.contains(type.trait) } as [Any] + let bottomViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] + let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] return topViewRotorElements + reusableViewRotorElements + remainingRotorElements + bottomViewRotorElements } @@ -152,7 +171,7 @@ class RotorHandler { traitIndexPath = indexPath } var result = result - if (model.accessibilityTraits?.contains(type.trait) ?? false), let traitIndexPath { + if type.modelFilter(model), let traitIndexPath { result.append(.init(indexPath: traitIndexPath, model: model)) } return result @@ -179,7 +198,7 @@ class RotorHandler { let controller = self.delegate as? RotorListTypeDelegateProtocol { controller.scrollToRow(at: element.indexPath, at: .middle, animated: false) guard let cellView = controller.cellForRow(at: element.indexPath) else { return UIAccessibilityCustomRotorItemResult() } - rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView]).filter { $0.accessibilityTraits.contains(type.trait) && $0.model?.id == element.model.id }.first as Any + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView]).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any } else { if let viewElement = (rotorElement as? UIView) { let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift index ca0acd79..9618cfab 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift @@ -15,7 +15,7 @@ public enum CheckboxPosition: String, Codable { case bottom } -@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol { +@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { open class var identifier: String { "checkboxLabel" } public var moleculeName: String = CheckboxLabelModel.identifier @DecodableDefault.UUIDString public var id: String @@ -25,6 +25,7 @@ public enum CheckboxPosition: String, Codable { public var checkbox: CheckboxModel public var label: LabelModel + public var children: [MoleculeModelProtocol] { [checkbox, label] } //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- From 436d4c746bd6af2a4dbc96b6ba94163272882514 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 19 Oct 2023 10:35:27 +0530 Subject: [PATCH 26/34] review comments and enhancements in rotorhandler --- .../Accessibility/AccessibilityHandler.swift | 11 +- MVMCoreUI/Accessibility/RotorHandler.swift | 134 +++++++++++++----- .../Protocols/MoleculeListProtocol.swift | 6 + .../Atomic/Templates/CollectionTemplate.swift | 8 ++ .../Templates/MoleculeListTemplate.swift | 9 +- .../ThreeLayerCollectionViewController.swift | 10 +- .../ThreeLayerViewController.swift | 8 +- 7 files changed, 127 insertions(+), 59 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 488ae403..32d6a0f2 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -22,13 +22,13 @@ public class AccessibilityOperation: MVMCoreOperation { } public override func main() { - guard UIAccessibility.isVoiceOverRunning, !checkAndHandleForCancellation() else { - stop() + guard UIAccessibility.isVoiceOverRunning else { + markAsFinished() return } Task { @MainActor [weak self] in guard let self = self, !self.isCancelled else { - self?.stop() + self?.markAsFinished() return } UIAccessibility.post(notification: self.notificationType, argument: self.argument) @@ -41,11 +41,6 @@ public class AccessibilityOperation: MVMCoreOperation { } } } - - public func stop() { - guard isCancelled else { return } - markAsFinished() - } } open class AccessibilityHandler { diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 5819b198..3c3fdbce 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -67,13 +67,22 @@ public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol { } public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol { - func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) -> Void + func scrollToRow(at indexPath: IndexPath, animated: Bool) -> Void func cellForRow(at indexPath: IndexPath) -> UIView? + func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? } struct RotorElement { - let indexPath: IndexPath + + let indexPath: IndexPath? let model: MoleculeModelProtocol + let carouselItemModel: MoleculeModelProtocol? + + init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) { + self.indexPath = indexPath + self.model = model + self.carouselItemModel = carouselItemModel + } } class RotorHandler { @@ -94,7 +103,9 @@ class RotorHandler { NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) .sink { [weak self] _ in guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return } - self?.identifyAndPrepareRotors(self?.delegateObject) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.identifyAndPrepareRotors(self?.delegateObject) + } }.store(in: &anyCancellable) } @@ -120,7 +131,7 @@ class RotorHandler { customRotors.append(rotor) } } - currentViewController.accessibilityCustomRotors = customRotors + currentViewController.view.accessibilityCustomRotors = customRotors } private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { @@ -143,40 +154,50 @@ class RotorHandler { } private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { - guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & UIViewController) else { - return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] + guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & ViewController) else { + return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] //BAU pages } - let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { type.filter($0) } as [Any] - var reusableViewRotorElements: [Any] = [] - if let middleView = template.middleView { - if middleView.isKind(of: UITableView.self), - let template = template as? (any MoleculeListProtocol & ViewController) { - reusableViewRotorElements = getRotorElements(from: template, type: type) ?? [] - } else { - reusableViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [middleView].compactMap { $0 }).filter { type.filter($0) } as [Any] - } - } - let bottomViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] - let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] - return topViewRotorElements + reusableViewRotorElements + remainingRotorElements + bottomViewRotorElements + let rotorElements = getRotorElements(from: template, type: type) + let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] //Other elements added to view if any. + return rotorElements + remainingRotorElements } - private func getRotorElements(from template: (any MoleculeListProtocol & ViewController)?, type: RotorType) -> [Any]? { - guard let templateModel = template?.model, - let moleculeList = template else { return nil } - var rotorElements: [RotorElement] = [] + private func getRotorElements(from template: (MVMCoreViewControllerProtocol & ViewController & RotorViewElementsProtocol)?, type: RotorType) -> [Any] { + guard let templateModel = template?.model as? ThreeLayerModelBase else { return [] } + var rotorElements: [Any] = [] + if let headerView = template?.topView { + rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [headerView].compactMap { $0 }).filter { type.filter($0) }) + } + let molecules = templateModel.rootMolecules.filter { !($0.id == templateModel.header?.id || $0.id == templateModel.footer?.id) } + rotorElements.append(contentsOf: getRotorElements(from: molecules, template: template, type: type)) + if let bottomView = template?.bottomView { + rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [bottomView].compactMap { $0 }).filter { type.filter($0) }) + } + return rotorElements + } + + private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] { var traitIndexPath: IndexPath? - rotorElements = templateModel.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: rotorElements) { result, model, depth in - if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), let indexPath = moleculeList.getIndexPath(for: listModel) { + var carouselItemModel: MoleculeModelProtocol? + var carouselDepth: Int? + let rotorElements: [RotorElement] = molecules.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: []) { result, model, depth in + if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), + let indexPath = (template as? RotorListTypeDelegateProtocol)?.getIndexPathFor(molecule: listModel) { traitIndexPath = indexPath } + if let _carouselDepth = carouselDepth, depth < _carouselDepth { + carouselDepth = nil + carouselItemModel = nil + } + if model is CarouselModel { carouselDepth = depth } + carouselItemModel = model is CarouselItemModelProtocol ? model : carouselItemModel var result = result - if type.modelFilter(model), let traitIndexPath { - result.append(.init(indexPath: traitIndexPath, model: model)) + if type.modelFilter(model) { + result.append(.init(indexPath: traitIndexPath, model: model, carouselItemModel: carouselItemModel)) } return result } - return rotorElements + return rotorElements as [Any] } private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? { @@ -195,10 +216,28 @@ class RotorHandler { guard rotorIndex >= 0 else { return UIAccessibilityCustomRotorItemResult() } var rotorElement = elements[rotorIndex] if let element = rotorElement as? RotorElement, - let controller = self.delegate as? RotorListTypeDelegateProtocol { - controller.scrollToRow(at: element.indexPath, at: .middle, animated: false) - guard let cellView = controller.cellForRow(at: element.indexPath) else { return UIAccessibilityCustomRotorItemResult() } - rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView]).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any + let controller = self.delegate as? (RotorViewElementsProtocol & ViewController) { + var cellView: UIView? + if let indexPath = element.indexPath, let controller = self.delegate as? (any RotorListTypeDelegateProtocol) { + controller.scrollToRow(at: indexPath, animated: false) + cellView = controller.cellForRow(at: indexPath) + } else { + cellView = controller.view + } + if let cauroselItemModel = element.carouselItemModel { + guard let carousel = MVMCoreUIUtility.findViews(by: Carousel.self, views: [cellView].compactMap { $0 }).first, + let index = (carousel.molecules?.firstIndex { $0.id == cauroselItemModel.id }) else { return UIAccessibilityCustomRotorItemResult() } + let collectionIndexPath = IndexPath(item: index, section: 0) + carousel.collectionView.scrollToItem(at: collectionIndexPath, at: .left, animated: false) + let collectionViewCell = carousel.collectionView.cellForItem(at: collectionIndexPath) + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [collectionViewCell].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any + } else { + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any + if let viewElement = (rotorElement as? UIView) { + let convertedFrame = viewElement.convert(viewElement.frame, to: controller.view) + (self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false) + } + } } else { if let viewElement = (rotorElement as? UIView) { let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view) @@ -223,19 +262,26 @@ extension RotorScrollDelegateProtocol { } extension RotorListTypeDelegateProtocol { - func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { } - func cellForRow(at indexPath: IndexPath) -> UIView? { nil } + + public func scrollToRow(at indexPath: IndexPath, animated: Bool) { } + public func cellForRow(at indexPath: IndexPath) -> UIView? { nil } + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { nil } } extension ThreeLayerTableViewController: RotorListTypeDelegateProtocol { - public func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { - tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: animated) + public func scrollToRow(at indexPath: IndexPath, animated: Bool) { + tableView.scrollToRow(at: indexPath, at: .middle, animated: animated) } public func cellForRow(at indexPath: IndexPath) -> UIView? { tableView.cellForRow(at: indexPath) } + + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { + guard let molecule = molecule as? (ListItemModelProtocol & MoleculeModelProtocol) else { return nil } + return (self as? MoleculeListTemplate)?.getIndexPath(for: molecule) + } } extension ThreeLayerViewController: RotorScrollDelegateProtocol { @@ -244,3 +290,19 @@ extension ThreeLayerViewController: RotorScrollDelegateProtocol { scrollView?.scrollRectToVisible(rect, animated: animated) } } + +extension ThreeLayerCollectionViewController: RotorListTypeDelegateProtocol { + + public func scrollToRow(at indexPath: IndexPath, animated: Bool) { + collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated) + } + + public func cellForRow(at indexPath: IndexPath) -> UIView? { + collectionView?.cellForItem(at: indexPath) + } + + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { + guard let molecule = molecule as? (CollectionItemModelProtocol & MoleculeModelProtocol) else { return nil } + return (self as? MoleculeCollectionListProtocol)?.getIndexPath(for: molecule) + } +} diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift index dfeb7b11..8f4bfee0 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift @@ -37,3 +37,9 @@ public extension MVMCoreUIDelegateObject { return (moleculeDelegate as? MoleculeListProtocol & NSObjectProtocol) } } + +public protocol MoleculeCollectionListProtocol { + + /// Asks the delegate for the index of molecule. + func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath? +} diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index b84792a6..3683e1e9 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -198,3 +198,11 @@ return modules } } + +extension CollectionTemplate: MoleculeCollectionListProtocol { + + public func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { + guard let index = (moleculesInfo?.firstIndex { $0.molecule.id == molecule.id }) else { return nil } + return IndexPath(item: index, section: 0) + } +} diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index a113ef5f..30a9b5c5 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -288,14 +288,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol /// Checks if the two molecules are equal private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool { - // TODO: move this to a better approach, maybe a UUID for each model. - // Do instance check - if let classMoleculeA = moleculeA as? NSObjectProtocol, - let classMoleculeB = moleculeB as? NSObjectProtocol { - return classMoleculeA === classMoleculeB - } - // ID check - return moleculeA.id == moleculeB.id + moleculeA.id == moleculeB.id } } diff --git a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift index 4011f4f8..05a575ae 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift @@ -9,10 +9,14 @@ import Foundation /// A view controller that has three main layers, a header, collection rows, and a footer. The header is added as a supplement header to the first section, and the footer is added as a supplement footer to the last section. This view controller allows for flexible space between the three layers to fit the screeen. -@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout { +@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout, RotorViewElementsProtocol { - private var topView: UIView? - private var bottomView: UIView? + public var topView: UIView? + public var middleView: UIView? { + set {} + get { collectionView } + } + public var bottomView: UIView? private var headerView: ContainerCollectionReusableView? private var footerView: ContainerCollectionReusableView? private let headerID = "header" diff --git a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift index ec8aaaa3..4560aa09 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift @@ -9,12 +9,12 @@ import UIKit -open class ThreeLayerViewController: ProgrammaticScrollViewController { +open class ThreeLayerViewController: ProgrammaticScrollViewController, RotorViewElementsProtocol { // The three main views - var topView: UIView? - var middleView: UIView? - var bottomView: UIView? + public var topView: UIView? + public var middleView: UIView? + public var bottomView: UIView? var useMargins: Bool = false // The top view can be put outside of the scrolling area. From b66b49d173c84b5c2970976bd9e5a7ed30a2ead5 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 19 Oct 2023 18:00:20 +0530 Subject: [PATCH 27/34] Added comments for methods --- .../Accessibility/AccessibilityHandler.swift | 102 +++++++++++++----- MVMCoreUI/Accessibility/RotorHandler.swift | 64 ++++++++++- .../Atomic/Organisms/Carousel/Carousel.swift | 2 +- 3 files changed, 138 insertions(+), 30 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 32d6a0f2..29b727b6 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -11,6 +11,7 @@ import Combine import MVMCore import WebKit +//MARK: - AccessibilityOperation public class AccessibilityOperation: MVMCoreOperation { private let argument: Any? @@ -21,6 +22,10 @@ public class AccessibilityOperation: MVMCoreOperation { self.argument = argument } + /** + This method will post accessibility notification. + If we have announcement notification then operation will wait untill announcement is finished. + */ public override func main() { guard UIAccessibility.isVoiceOverRunning else { markAsFinished() @@ -32,7 +37,7 @@ public class AccessibilityOperation: MVMCoreOperation { return } UIAccessibility.post(notification: self.notificationType, argument: self.argument) - if self.notificationType == .announcement { + if self.notificationType == .announcement {///Marking task as finished only if announcement did finished so that user will listen complete announcement. NotificationCenter.default.addObserver(forName: UIAccessibility.announcementDidFinishNotification, object: nil, queue: .main) { _ in self.markAsFinished() } @@ -43,14 +48,19 @@ public class AccessibilityOperation: MVMCoreOperation { } } +//MARK: - AccessibilityHandler +/** + AccessibilityHandler will observe the page visibility of every view controller and post notification to the first interactive element on the screen. + If we have to shift/foucs custom element on the screen on controller shown then we need to pass accessibilityId in the page response. + */ open class AccessibilityHandler { public static func shared() -> Self? { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } - public var accessibilityId: String? - public var previousAccessiblityElement: Any? + public var accessibilityId: String? ///This property is used to post accessibility to the UIElement mapped to this accessibilityId + public var previousAccessiblityElement: Any? ///This property is capture accessiblity element public var anyCancellable: Set = [] public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } public weak var delegateObject: MVMCoreUIDelegateObject? @@ -63,34 +73,52 @@ open class AccessibilityHandler { lazy var rotorHandler = RotorHandler(accessibilityHandler: self) + /** + init method will register for focus changes + */ public init() { registerForFocusChanges() } - // MARK: - Accessibility Handler operation events + /** + This method will capture current foucsed element + */ open func capturePreviousFocusElement() { previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) } - open func postAccessbilityToPrevElement() { + /** + This method will post accessibility notification to previous captured element + */ + open func postAccessibilityToPrevElement() { post(notification: .layoutChanged, argument: previousAccessiblityElement) previousAccessiblityElement = nil } - + + /** + This method will check if voice over is running then will post notification to the mentioned argument + */ public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } let AccessibilityOperation = AccessibilityOperation(notificationType: type, argument: argument) accessibilityOperationQueue.addOperation(AccessibilityOperation) } - //To get first focus element on the screen + /** + This method return first focused element from the screen. + */ open func getFirstFocusedElementOnScreen() -> Any? { (delegate as? UIViewController)?.navigationController?.navigationBar } - //Subclass can decide to trigger Accessibility notification on screen change. - open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } + /** + This method is used to decide if AccessibilityHandler can post screen change notification or specific classes will take care of posting Accessibility notification + */ + open func canPostAccessibilityNotification(for viewController: UIViewController) -> Bool { true } + /** + This method is used to identify the UIElement that is mapped to accessibilityId from server response. + */ func getPreDefinedFocusedElementIfAny() -> UIView? { guard let accessibilityId, let view = (delegate as? UIViewController)?.view else { return nil } return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first { @@ -99,13 +127,12 @@ open class AccessibilityHandler { } } -/** - When we push a new viewcontroller on to a Navigation stack from iOS 13+ Accessibility voiceover is going to the element inside of the viewcontroller. Not treating navigationController left/back bar button as first element. So alternatively we are setting accessibility elements in viewWillAppear untill viewDidAppear then we are resetting back So that there will not be accessibility order issue. - https://developer.apple.com/forums/thread/655359 - https://developer.apple.com/forums/thread/675427 - */ extension AccessibilityHandler { + /** + This method is used to notify rotor handler about page loaded and capturing the accessibilityId if any. + Will announce text if the page is has announcementText in the response. + */ public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { previousAccessiblityElement = nil rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject) @@ -117,11 +144,16 @@ extension AccessibilityHandler { self.delegateObject = delegateObject } - //MARK: - PageVisibiltyBehaviour + /** + This method is used to capture accessibility views on the screen. + If the page has accessibilityId, then it will not post any accessibility notification because respective UI mapped element can be identified only on page shown. + If it has top notification then we are capturing the first focused element and will not post any accessibility notification. + If page doesn't have any top notification or accessibilityId then it will post notification to shift foucs to first focused element on the screen. + */ public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) guard let controller = delegateObject?.moleculeDelegate as? UIViewController, - canPostAccessbilityNotification(for: controller), + canPostAccessibilityNotification(for: controller), accessibilityId == nil else { return } if hasTopNotificationInPage { previousAccessiblityElement = getFirstFocusedElementOnScreen() @@ -130,8 +162,15 @@ extension AccessibilityHandler { } } - ///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 + /** + This method is used to notify rotor handler about page visibility + Temp fix: - We are resetting the view accessibilityElements when focus shifted to first focused element on the screen not to have voice over struck in between view elements. + https://developer.apple.com/forums/thread/655359 + https://developer.apple.com/forums/thread/675427 + If the page has accessibilityId, i.e if server decides to manually shift focus to one of the UIElement then we are identifying the id mapped UIElement & shifting focus to that element. + If we have top notification as well in the page then we take that as priority and holding the server driven UIElement in previousAccessiblityElement and post accessibility notification. + https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain + */ public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { defer { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -148,7 +187,9 @@ extension AccessibilityHandler { } } - //MARK: - Accessibility Methods + /** + This method is used to set view elements as accessibilityElements due to the accessibility behaviour change when new controller is pushed on navigation stack from iOS13+ + */ private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { guard var currentController = delegateObject?.moleculeDelegate as? UIViewController, currentController.navigationController != nil else { return }///If no navigationController, we can go with the default behaviour of Accessibility voiceover. @@ -166,9 +207,13 @@ extension AccessibilityHandler { } } +// MARK: - AccessibilityHandler listeners @objc extension AccessibilityHandler { - // MARK: - Register with Accessibility Handler listeners + /** + This method listens for foucs changes. + When foucs is changed manually then we are cancelling existing operations. + */ private func registerForFocusChanges() { //Since focus shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) @@ -177,6 +222,13 @@ extension AccessibilityHandler { }.store(in: &anyCancellable) } + /** + This method listens for top notification changes. + When top notification is about to display it will capture previous focused element. + When top notification is displayed it will post notification to that notification view + When top notification is about to dismiss then it will post announcement that top alert is closed. + When top notification is dimissed then it will post notification back to previously captured element. + */ func registerForTopNotificationsChanges() { NotificationHandler.shared()?.onNotificationWillShow .sink { [weak self] (_, model) in @@ -194,13 +246,15 @@ extension AccessibilityHandler { }.store(in: &anyCancellable) NotificationHandler.shared()?.onNotificationDismissed .sink { [weak self] (view, model) in - self?.postAccessbilityToPrevElement() + self?.postAccessibilityToPrevElement() }.store(in: &anyCancellable) } } -// 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. +// MARK: - AccessibilityHandlerBehaviour +/** + To notify AccessibilityHandler about the page visibility changes + */ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior { public let accessibilityHandler: AccessibilityHandler? @@ -213,12 +267,10 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method. } - //MARK: - PageMoleculeTransformationBehavior open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject) } - //MARK: - PageVisibiltyBehaviour open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { accessibilityHandler?.willShowPage(delegateObject) } diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 3c3fdbce..916b4c8d 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -9,11 +9,13 @@ import Foundation import Combine +//MARK: - RotorType that our app supports fileprivate enum RotorType: String, CaseIterable { case button = "Buttons" case header = "Header" case checkbox = "Checkbox" + case link = "Link" var trait: UIAccessibilityTraits { switch self { @@ -21,11 +23,14 @@ fileprivate enum RotorType: String, CaseIterable { return .button case .header: return .header + case .link: + return .link default: return .none } } + ///Filter block on model elements based on rotor type var modelFilter: ((MoleculeModelProtocol) -> Bool) { switch self { case .checkbox: @@ -35,6 +40,7 @@ fileprivate enum RotorType: String, CaseIterable { } } + ///Filter block on model UIElements based on rotor type var filter: ((UIView) -> Bool) { switch self { case .checkbox: @@ -44,6 +50,7 @@ fileprivate enum RotorType: String, CaseIterable { } } + ///Returns custom rotor of the specific type func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? { var accessibilityCustomRotor: UIAccessibilityCustomRotor? switch self { @@ -56,12 +63,14 @@ fileprivate enum RotorType: String, CaseIterable { } } +//MARK: - RotorViewElementsProtocol public protocol RotorViewElementsProtocol: MVMCoreViewControllerProtocol { var topView: UIView? { get set } var middleView: UIView? { get set } var bottomView: UIView? { get set } } +//MARK: - RotorProtocols public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol { func scrollRectToVisible(_ rect: CGRect, animated: Bool) } @@ -72,11 +81,12 @@ public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol { func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? } +//MARK: - Rotor Element - RotorElement info when we are traversing on model elements in the page. struct RotorElement { let indexPath: IndexPath? let model: MoleculeModelProtocol - let carouselItemModel: MoleculeModelProtocol? + let carouselItemModel: MoleculeModelProtocol?///This element is the parent of model item if we have rotor element inside carousel. This is used to scroll to specific carousel item when rotor mode is enabled. init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) { self.indexPath = indexPath @@ -85,6 +95,7 @@ struct RotorElement { } } +//MARK: - Rotor Handler class RotorHandler { private var rotorIndexes: [RotorType: Int] = [:] @@ -99,6 +110,7 @@ class RotorHandler { registerForVoiceOverChanges() } + ///Preparing rotors when accessibility voiceover is turned after page is loaded. private func registerForVoiceOverChanges() { NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) .sink { [weak self] _ in @@ -109,6 +121,7 @@ class RotorHandler { }.store(in: &anyCancellable) } + //MARK: - Pagevisibility behaviour methods. public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { rotorIndexes = [:] rotorElements = [:] @@ -119,6 +132,11 @@ class RotorHandler { identifyAndPrepareRotors(delegateObject) } + //MARK: - Rotor methods + /** + This method prepares custom rotors that will be assigned to the current controller. + Rotor will be created only if the page contains that trait mapped element or the conditions met. + */ private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) { guard UIAccessibility.isVoiceOverRunning, let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return } @@ -134,6 +152,9 @@ class RotorHandler { currentViewController.view.accessibilityCustomRotors = customRotors } + /** + This method prepares trait mapped elements of the current controller and from its manager if exists. + */ private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { var accessibilityElements: [Any?] = [] if let manager = (template as? MVMCoreViewManagerViewControllerProtocol)?.manager as? (MVMCoreViewControllerProtocol & UIViewController) { @@ -148,11 +169,19 @@ class RotorHandler { return accessibilityElements.compactMap { $0 } } + /** + This method prepares trait mapped elements from manager + */ private func getRotorElementsFrom(manager: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { guard let elements = (manager as? MVMCoreViewManagerProtocol)?.getAccessibilityElements() as? [UIView] else { return nil } return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { type.filter($0) } as [Any] } + /** + This method prepares triat mapped elements from the current viewcontroller. + For BAU pages: This will traverse through the view hierarchy for trait mapped elements. + For Molecular pages: This will traverse through the models to identify the trait mapped model. Along with traversed models, again we are traversing on view hierarchy if any subViews are added to view to get trait mapped elements. + */ private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & ViewController) else { return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] //BAU pages @@ -162,6 +191,10 @@ class RotorHandler { return rotorElements + remainingRotorElements } + /** + This method is to get rotor elements form Molecular pages only. + We are filtering out header, molecules, footer from rootMolcules because rootMolcules may or maynot have the correct order of molecule models(header, body, footer). If we don't maintain order, Voiceover might first go footer then body elements then header.(Safety step) + */ private func getRotorElements(from template: (MVMCoreViewControllerProtocol & ViewController & RotorViewElementsProtocol)?, type: RotorType) -> [Any] { guard let templateModel = template?.model as? ThreeLayerModelBase else { return [] } var rotorElements: [Any] = [] @@ -176,6 +209,11 @@ class RotorHandler { return rotorElements } + /** + This method actually travers through the molecules and identify triat mapped model element along with indexPath of the element if its List/Collection templates. + Along with model, indexPath we are also capturing carouselItemModel because when we have Carousel inside the molecule, we need to scroll to carousel item if we have trait mapped rotor element inside the Carousel - (Multi Scroll behaviour) + Identiying the CarouselModel, CarouselItemModel by depth. + */ private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] { var traitIndexPath: IndexPath? var carouselItemModel: MoleculeModelProtocol? @@ -200,20 +238,37 @@ class RotorHandler { return rotorElements as [Any] } + /** + This method creates a rotor based on the RotorType. + UIAccessibilityCustomRotor.Search Predicate block is used to return the current rotor UI element + If the rotor element is of type UIElement(subclasses of UIView) then it will post the accessibility notification directly to that UI element and scrolling that element to the visible area. + If rotor element is of type RotorElement then + 1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view. + 2. After the cell view is captured, traversing the cell hierarchy which matches the trait & id of that view's model. + 3. After identifying the element, then will post the accessibility notification directly to that UI element + If we have carouselItemModel in RotorElement then + 1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view. + 2. After cell is identified then we are identifying Carousel from the view hierarchy & scroll to the Carousel item which matches the carouselItemModel.id + 3. After carouselItemModel is scrolled then traversing the carouselCellItem hierarchy which matches the trait & id of that view's model + 4. After identifying the element, then will post the accessibility notification directly to that UI element + */ private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? { return type.getUIAccessibilityCustomRotor { [weak self] predicate in guard let self, let elements = self.rotorElements[type] else { return UIAccessibilityCustomRotorItemResult() } var rotorIndex = self.rotorIndexes[type] ?? -1 - if predicate.searchDirection == .next { + switch predicate.searchDirection { + case .next: if rotorIndex + 1 < elements.count { rotorIndex += 1 } - } else { + case .previous: if rotorIndex > 0 { rotorIndex -= 1 } + @unknown default: + rotorIndex = 0 } - guard rotorIndex >= 0 else { return UIAccessibilityCustomRotorItemResult() } + guard rotorIndex >= 0, !elements.isEmpty else { return UIAccessibilityCustomRotorItemResult() } //Safety check to avoid crash. var rotorElement = elements[rotorIndex] if let element = rotorElement as? RotorElement, let controller = self.delegate as? (RotorViewElementsProtocol & ViewController) { @@ -251,6 +306,7 @@ class RotorHandler { } } +//MARK: - Protocol Extensions public extension RotorViewElementsProtocol { var topView: UIView? { nil } var middleView: UIView? { nil } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index ea7d89b8..6b360120 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -384,7 +384,7 @@ open class Carousel: View { extension Carousel: UICollectionViewDelegateFlowLayout { open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - let itemWidth = collectionView.bounds.width * itemWidthPercent + let itemWidth = (collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right) * itemWidthPercent return CGSize(width: itemWidth, height: collectionView.bounds.height) } From e48db9495d4e58e4037efc51b4e24457bce20af9 Mon Sep 17 00:00:00 2001 From: Keerthy Date: Mon, 30 Oct 2023 13:54:48 +0530 Subject: [PATCH 28/34] Minor refactoring --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 29b727b6..73047b06 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -24,9 +24,9 @@ public class AccessibilityOperation: MVMCoreOperation { /** This method will post accessibility notification. - If we have announcement notification then operation will wait untill announcement is finished. */ public override func main() { + guard !checkAndHandleForCancellation() else { return } guard UIAccessibility.isVoiceOverRunning else { markAsFinished() return @@ -37,13 +37,7 @@ public class AccessibilityOperation: MVMCoreOperation { return } UIAccessibility.post(notification: self.notificationType, argument: self.argument) - if self.notificationType == .announcement {///Marking task as finished only if announcement did finished so that user will listen complete announcement. - NotificationCenter.default.addObserver(forName: UIAccessibility.announcementDidFinishNotification, object: nil, queue: .main) { _ in - self.markAsFinished() - } - } else { - self.markAsFinished() - } + self.markAsFinished() } } } @@ -131,16 +125,12 @@ extension AccessibilityHandler { /** This method is used to notify rotor handler about page loaded and capturing the accessibilityId if any. - Will announce text if the page is has announcementText in the response. */ public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { previousAccessiblityElement = nil rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject) guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return } accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") - if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") { - post(notification: .announcement, argument: announcementText) - } self.delegateObject = delegateObject } From 92b7c3ef363cbc4dc047fd724b95934c0eae6e15 Mon Sep 17 00:00:00 2001 From: Keerthy Date: Wed, 1 Nov 2023 11:19:30 +0530 Subject: [PATCH 29/34] foucs spelling mistake --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 73047b06..d8053cf9 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -45,7 +45,7 @@ public class AccessibilityOperation: MVMCoreOperation { //MARK: - AccessibilityHandler /** AccessibilityHandler will observe the page visibility of every view controller and post notification to the first interactive element on the screen. - If we have to shift/foucs custom element on the screen on controller shown then we need to pass accessibilityId in the page response. + If we have to shift/focus custom element on the screen on controller shown then we need to pass accessibilityId in the page response. */ open class AccessibilityHandler { @@ -75,7 +75,7 @@ open class AccessibilityHandler { } /** - This method will capture current foucsed element + This method will capture current focused element */ open func capturePreviousFocusElement() { previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) @@ -138,7 +138,7 @@ extension AccessibilityHandler { This method is used to capture accessibility views on the screen. If the page has accessibilityId, then it will not post any accessibility notification because respective UI mapped element can be identified only on page shown. If it has top notification then we are capturing the first focused element and will not post any accessibility notification. - If page doesn't have any top notification or accessibilityId then it will post notification to shift foucs to first focused element on the screen. + If page doesn't have any top notification or accessibilityId then it will post notification to shift focus to first focused element on the screen. */ public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { updateAccessibilityViews(delegateObject) @@ -201,8 +201,8 @@ extension AccessibilityHandler { @objc extension AccessibilityHandler { /** - This method listens for foucs changes. - When foucs is changed manually then we are cancelling existing operations. + This method listens for focus changes. + When focus is changed manually then we are cancelling existing operations. */ private func registerForFocusChanges() { //Since focus shifted to other elements cancelling existing focus shift notifications if any From 975882d47cbb50d106755532e0287b998d8b58f5 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 1 Nov 2023 18:11:22 +0530 Subject: [PATCH 30/34] addressed code review comments --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 4 +++- MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index d8053cf9..96053673 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -100,9 +100,10 @@ open class AccessibilityHandler { /** This method return first focused element from the screen. + If navigationBar is hidden then we are returning nil so that voice over will shift to the first interactive element. */ open func getFirstFocusedElementOnScreen() -> Any? { - (delegate as? UIViewController)?.navigationController?.navigationBar + ((delegate as? PageProtocol)?.pageModel?.navigationBar?.hidden ?? false) ? nil : (delegate as? UIViewController)?.navigationController?.navigationBar } /** @@ -245,6 +246,7 @@ extension AccessibilityHandler { /** To notify AccessibilityHandler about the page visibility changes */ +//TODO: Revisit why we need a behavior as a notifier. open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior { public let accessibilityHandler: AccessibilityHandler? diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index 008c2911..f4ce9234 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,7 +7,7 @@ // -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Identifiable { +public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- From 5d7dab8879468bee10d92203b70f4dac5c58372d Mon Sep 17 00:00:00 2001 From: Keerthy Date: Wed, 1 Nov 2023 22:51:04 +0530 Subject: [PATCH 31/34] Removed checkbox from rotor --- MVMCoreUI/Accessibility/RotorHandler.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 916b4c8d..d529564b 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -14,7 +14,6 @@ fileprivate enum RotorType: String, CaseIterable { case button = "Buttons" case header = "Header" - case checkbox = "Checkbox" case link = "Link" var trait: UIAccessibilityTraits { @@ -33,8 +32,6 @@ fileprivate enum RotorType: String, CaseIterable { ///Filter block on model elements based on rotor type var modelFilter: ((MoleculeModelProtocol) -> Bool) { switch self { - case .checkbox: - return { $0 is CheckboxModel } default: return { $0.accessibilityTraits?.contains(trait) ?? false } } @@ -43,8 +40,6 @@ fileprivate enum RotorType: String, CaseIterable { ///Filter block on model UIElements based on rotor type var filter: ((UIView) -> Bool) { switch self { - case .checkbox: - return { $0 is Checkbox } default: return { $0.accessibilityTraits.contains(trait) } } From df2fa3405b863ed01458a6f53dbfe7f714d6b387 Mon Sep 17 00:00:00 2001 From: Keerthy Date: Thu, 2 Nov 2023 18:46:27 +0530 Subject: [PATCH 32/34] Updated delegateObject and delegate properties removed weak ref for delegateObject. updated delegate as delegate.loadDelegate --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 96053673..2cfcf4a9 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -56,8 +56,8 @@ open class AccessibilityHandler { public var accessibilityId: String? ///This property is used to post accessibility to the UIElement mapped to this accessibilityId public var previousAccessiblityElement: Any? ///This property is capture accessiblity element public var anyCancellable: Set = [] - public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol } - public weak var delegateObject: MVMCoreUIDelegateObject? + public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol } + public var delegateObject: MVMCoreUIDelegateObject? private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil } private let accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() From 9c43649cceb86b81aacbf994fd59c7b931693524 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 2 Nov 2023 21:40:01 +0530 Subject: [PATCH 33/34] added review comments --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 2cfcf4a9..65ae8272 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -53,12 +53,14 @@ open class AccessibilityHandler { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } + //TODO: Revisit to avoid state properties to store in handler. public var accessibilityId: String? ///This property is used to post accessibility to the UIElement mapped to this accessibilityId public var previousAccessiblityElement: Any? ///This property is capture accessiblity element public var anyCancellable: Set = [] - public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol } + public weak var currentController: UIViewController? { MVMCoreUIUtility.getCurrentVisibleController() } public var delegateObject: MVMCoreUIDelegateObject? - private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil } + //TODO: Revisit to identify the page has top notification or not. + private var hasTopNotificationInPage: Bool { (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil } private let accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 @@ -103,7 +105,7 @@ open class AccessibilityHandler { If navigationBar is hidden then we are returning nil so that voice over will shift to the first interactive element. */ open func getFirstFocusedElementOnScreen() -> Any? { - ((delegate as? PageProtocol)?.pageModel?.navigationBar?.hidden ?? false) ? nil : (delegate as? UIViewController)?.navigationController?.navigationBar + ((currentController as? PageProtocol)?.pageModel?.navigationBar?.hidden ?? false) ? nil : currentController?.navigationController?.navigationBar } /** @@ -115,7 +117,7 @@ open class AccessibilityHandler { This method is used to identify the UIElement that is mapped to accessibilityId from server response. */ func getPreDefinedFocusedElementIfAny() -> UIView? { - guard let accessibilityId, let view = (delegate as? UIViewController)?.view else { return nil } + guard let accessibilityId, let view = currentController?.view else { return nil } return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first { $0.model?.id == accessibilityId } From a3c4c2248cf8eaad445668dfd1186092cbfea3e7 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 2 Nov 2023 23:06:05 +0530 Subject: [PATCH 34/34] removed unused code & added hidden check --- MVMCoreUI/Accessibility/RotorHandler.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index d529564b..a4546980 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -24,8 +24,6 @@ fileprivate enum RotorType: String, CaseIterable { return .header case .link: return .link - default: - return .none } } @@ -41,7 +39,7 @@ fileprivate enum RotorType: String, CaseIterable { var filter: ((UIView) -> Bool) { switch self { default: - return { $0.accessibilityTraits.contains(trait) } + return { $0.accessibilityTraits.contains(trait) && !$0.isHidden } } }