From 3d556a850cace92b2e056e0dabda02abc05278b0 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Fri, 14 Jul 2023 22:46:22 +0530 Subject: [PATCH] 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() {