From 88505e704cab9b6ef93283311d34ad1e0597ffb1 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Wed, 4 Oct 2023 20:08:46 +0530 Subject: [PATCH] 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? {