enhancements for top alert post notification for accessibility

This commit is contained in:
Krishna Kishore Bandaru 2023-07-14 22:46:22 +05:30
parent ebb2c35c55
commit 3d556a850c
4 changed files with 50 additions and 43 deletions

View File

@ -9,10 +9,11 @@
import Foundation import Foundation
import Combine import Combine
import MVMCore import MVMCore
import WebKit
public enum AccessibilityNotificationType: String, Codable { 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. //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/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 //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 { var delay: Double {
switch self { switch self {
case .controllerChanged: case .controllerChanged, .webPageLoaded:
return 1.5 return 1.5
case .screenChanged, .layoutChanged: case .screenChanged, .layoutChanged:
return 0.0 return 0.0
@ -37,7 +38,7 @@ public enum AccessibilityNotificationType: String, Codable {
return .screenChanged return .screenChanged
case .layoutChanged, .controllerChanged: case .layoutChanged, .controllerChanged:
return .layoutChanged return .layoutChanged
case .webPageChanged: case .webPageChanged, .webPageLoaded:
return .layoutChanged return .layoutChanged
} }
} }
@ -87,7 +88,8 @@ open class AccessibilityHandler {
guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil }
return MVMCoreActionUtility.fatalClassCheck(object: shared) return MVMCoreActionUtility.fatalClassCheck(object: shared)
} }
public let webPageNavigated = PassthroughSubject<MVMCoreLoadObject?, Never>() public weak var delegate: MVMCoreViewControllerProtocol?
public var previousAccessiblityElement: Any?
private var accessibilityOperationQueue: OperationQueue = { private var accessibilityOperationQueue: OperationQueue = {
let queue = OperationQueue() let queue = OperationQueue()
@ -95,16 +97,15 @@ open class AccessibilityHandler {
return queue return queue
}() }()
private var anyCancellable: Set<AnyCancellable> = [] private var anyCancellable: Set<AnyCancellable> = []
private weak var delegate: MVMCoreViewControllerProtocol?
private var accessibilityId: String? private var accessibilityId: String?
private var previousAccessiblityElement: Any? private var announcementText: String?
private var hasTopNotitificationInPage: Bool = false
public init() { public init() {
registerWithNotificationCenter() registerWithNotificationCenter()
registerForPageChanges() registerForPageChanges()
registerForFocusChanges() registerForFocusChanges()
registerForTopNotificationsChanges() registerForTopNotificationsChanges()
registerForWebpageNavigation()
} }
/// Registers with the notification center to know when json is updated. /// 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)) NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
.sink { [weak self] notification in .sink { [weak self] notification in
self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") 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) }.store(in: &anyCancellable)
} }
@ -129,22 +131,31 @@ open class AccessibilityHandler {
} }
private func registerForTopNotificationsChanges() { 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 NotificationHandler.shared()?.onNotificationShown
.sink { [weak self] (view, model) in .sink { [weak self] (view, model) in
self?.post(notification: .layoutChanged, argument: view) self?.post(notification: .layoutChanged, argument: view)
}.store(in: &anyCancellable) }.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationDismissed NotificationHandler.shared()?.onNotificationWillDismiss
.sink { [weak self] (view, model) in .sink { [weak self] (view, model) in
self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) 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) }.store(in: &anyCancellable)
print(anyCancellable) print(anyCancellable)
} }
private func registerForWebpageNavigation() { open func capturePreviousFocusElement(for model: MoleculeModelProtocol) {
webPageNavigated.sink { [weak self] _ in previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver)
self?.post(notification: .layoutChanged, argument: self?.getFirstFocusedElementOnScreen()) }
}.store(in: &anyCancellable)
open func postAccessbilityToPrevElement(for model: MoleculeModelProtocol) {
post(notification: .layoutChanged, argument: previousAccessiblityElement)
} }
private func add(operation: Operation) { private func add(operation: Operation) {
@ -155,11 +166,19 @@ open class AccessibilityHandler {
accessibilityOperationQueue.cancelAllOperations() 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) { public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) {
guard UIAccessibility.isVoiceOverRunning else { return } guard UIAccessibility.isVoiceOverRunning else { return }
let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument)
add(operation: accessbilityOperation) 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. //Subclass can decide to trigger Accessibility notification on screen change.
@ -169,7 +188,12 @@ open class AccessibilityHandler {
extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { extension AccessibilityHandler: MVMCorePresentationDelegateProtocol {
public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) {
previousAccessiblityElement = nil
delegate = viewController as? MVMCoreViewControllerProtocol 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) { 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" { if let presentationStyle = delegate?.loadObject??.pageJSON?.optionalStringForKey(KeyPresentationStyle) ?? delegate?.loadObject??.requestParameters?.actionMap?.optionalStringForKey(KeyPresentationStyle), presentationStyle == "root" {
navigationOperationType = .set navigationOperationType = .set
}*/ }*/
let accessbilityElement = getAccessbilityFocusedElement() if hasTopNotitificationInPage {
post(notification: navigationController.children.count == 1 ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) previousAccessiblityElement = getFirstFocusedElementOnScreen()
accessibilityId = nil } 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 return true
}.first }.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 { extension UIView {

View File

@ -7,7 +7,7 @@
// //
open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol { open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol, AccessibilityElementProtocol {
/** /**
The style of the notification: The style of the notification:
@ -35,18 +35,20 @@ open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol {
public var button: ButtonModel? public var button: ButtonModel?
public var closeButton: NotificationXButtonModel? public var closeButton: NotificationXButtonModel?
public var style: NotificationMoleculeModel.Style = .success public var style: NotificationMoleculeModel.Style = .success
public var id: String?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializer // 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.headline = headline
self.style = style self.style = style
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.body = body self.body = body
self.button = button self.button = button
self.closeButton = closeButton self.closeButton = closeButton
self.id = id
super.init() super.init()
} }

View File

@ -25,16 +25,6 @@ public class NotificationContainerView: UIView {
super.init(coder: coder) super.init(coder: coder)
setupView() 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 { extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@ -56,7 +46,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
self.superview?.layoutIfNeeded() self.superview?.layoutIfNeeded()
} completion: { finished in } completion: { finished in
self.superview?.layoutIfNeeded() self.superview?.layoutIfNeeded()
self.updateAccessibilityForTopAlert(notification)
continuation.resume() continuation.resume()
} }
} }
@ -64,14 +53,11 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@MainActor @MainActor
public func hide(notification: UIView) async { 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 await withCheckedContinuation { continuation in
UIView.animate(withDuration: 0.5) { UIView.animate(withDuration: 0.5) {
self.height.isActive = true self.height.isActive = true
self.superview?.layoutIfNeeded() self.superview?.layoutIfNeeded()
} completion: { finished in } completion: { finished in
UIAccessibility.post(notification: .layoutChanged, argument: nil)
self.currentNotificationView?.removeFromSuperview() self.currentNotificationView?.removeFromSuperview()
self.currentNotificationView = nil self.currentNotificationView = nil
continuation.resume() continuation.resume()

View File

@ -11,11 +11,7 @@ import MVMCore
@objcMembers open class CoreUIObject: MVMCoreObject { @objcMembers open class CoreUIObject: MVMCoreObject {
public var alertHandler: AlertHandler? public var alertHandler: AlertHandler?
public var topNotificationHandler: NotificationHandler? { public var topNotificationHandler: NotificationHandler?
didSet {
accessibilityHandler = AccessibilityHandler()
}
}
public var accessibilityHandler: AccessibilityHandler? public var accessibilityHandler: AccessibilityHandler?
open override func defaultInitialSetup() { open override func defaultInitialSetup() {