addressed review comments & added model property to MoleculeViewProtocol

This commit is contained in:
Krishna Kishore Bandaru 2023-10-11 00:47:31 +05:30
parent 88505e704c
commit 8dc475ffdd
11 changed files with 205 additions and 198 deletions

View File

@ -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<AnyCancellable> = []
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<AnyCancellable> = []
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<AnyCancellable> = []
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)
}
}

View File

@ -418,8 +418,3 @@ extension Toggle {
public func horizontalAlignment() -> UIStackView.Alignment { .trailing }
}
extension Toggle: MoleculeViewModelProtocol {
public var moleculeModel: MoleculeModelProtocol? { model }
}

View File

@ -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 }
}

View File

@ -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) {

View File

@ -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]?

View File

@ -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 { }

View File

@ -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 }
}
}

View File

@ -165,8 +165,3 @@ extension Button: AppleGuidelinesProtocol {
Self.acceptablyOutsideBounds(point: point, bounds: bounds)
}
}
extension Button: MoleculeViewModelProtocol {
public var moleculeModel: MoleculeModelProtocol? { model }
}

View File

@ -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)

View File

@ -249,6 +249,11 @@ public extension MVMCoreUISplitViewController {
}
extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol {
public func getAccessibilityElements() -> [Any]? {
nil
}
public func getCurrentViewController() -> UIViewController? {
navigationController?.getCurrentViewController()
}

View File

@ -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)