Added RotorHandler & addressed review comments

This commit is contained in:
Krishna Kishore Bandaru 2023-10-13 22:28:00 +05:30
parent 57d83b4884
commit 521eaa7c15
8 changed files with 283 additions and 157 deletions

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 52; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -169,6 +169,7 @@
52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; }; 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; };
52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; };
7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.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 */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; };
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; }; 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; };
8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.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 = "<group>"; }; 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethod.swift; sourceTree = "<group>"; };
52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = "<group>"; }; 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = "<group>"; };
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; }; 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; };
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = "<group>"; };
8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = "<group>"; }; 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = "<group>"; };
8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = "<group>"; }; 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = "<group>"; };
8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextBodyTextModel.swift; sourceTree = "<group>"; }; 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextBodyTextModel.swift; sourceTree = "<group>"; };
@ -1457,6 +1459,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */, 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */,
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */,
); );
path = Accessibility; path = Accessibility;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3012,6 +3015,7 @@
D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */, D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */,
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */, D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */,
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */,
AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */, AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */,
8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */, 8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */,
D243859923A16B1800332775 /* Container.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */,

View File

@ -49,49 +49,24 @@ public class AccessbilityOperation: MVMCoreOperation {
} }
open class AccessibilityHandler { 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? { public static func shared() -> Self? {
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 var accessibilityId: String? public var accessibilityId: String?
public var previousAccessiblityElement: Any? public var previousAccessiblityElement: Any?
public var anyCancellable: Set<AnyCancellable> = [] public var anyCancellable: Set<AnyCancellable> = []
public weak var delegate: MVMCoreViewControllerProtocol? public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol }
private var rotorIndexes: [RotorType: Int] = [:] public weak var delegateObject: MVMCoreUIDelegateObject?
private var hasTopNotificationInPage: Bool { NotificationHandler.shared()?.isNotificationShowing() ?? false } private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("userMessage") != nil }
private let accessibilityOperationQueue: OperationQueue = { private let accessibilityOperationQueue: OperationQueue = {
let queue = OperationQueue() let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1 queue.maxConcurrentOperationCount = 1
return queue return queue
}() }()
lazy var rotorHandler = RotorHandler(accessibilityHandler: self)
public init() { public init() {
registerForFocusChanges() 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 { extension AccessibilityHandler {
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
rotorIndexes = [:]
previousAccessiblityElement = nil previousAccessiblityElement = nil
rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject)
guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return } guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return }
accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId") accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId")
if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") { if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") {
post(notification: .announcement, argument: announcementText) post(notification: .announcement, argument: announcementText)
} }
delegate = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol self.delegateObject = delegateObject
} }
//MARK: - PageVisibiltyBehaviour //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. ///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 ///https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
identifyAndPrepareRotors(delegateObject) defer {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
(delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil (delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil
}
} }
rotorHandler.onPageShown(delegateObject)
guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return } guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return }
accessibilityId = nil accessibilityId = nil
if hasTopNotificationInPage { if hasTopNotificationInPage {
@ -173,115 +155,19 @@ extension AccessibilityHandler {
//MARK: - Accessibility Methods //MARK: - Accessibility Methods
private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) { 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] var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController]
if let manager = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? MVMCoreViewManagerProtocol & UIViewController), 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: managerAccessibilityElements)
accessibilityElements.append(contentsOf: manager.view.subviews) accessibilityElements.append(contentsOf: manager.view.subviews)
currentController = manager currentController = manager
} else { } else {
accessibilityElements.append(contentsOf: currentController?.view.subviews ?? []) accessibilityElements.append(contentsOf: currentController.view.subviews)
} }
accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar)
currentController?.view.accessibilityElements = accessibilityElements.compactMap { $0 } 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)
}
} }
} }
@ -309,7 +195,7 @@ extension AccessibilityHandler {
}.store(in: &anyCancellable) }.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationWillDismiss NotificationHandler.shared()?.onNotificationWillDismiss
.sink { [weak self] (view, model) in .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) }.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationDismissed NotificationHandler.shared()?.onNotificationDismissed
.sink { [weak self] (view, model) in .sink { [weak self] (view, model) in
@ -333,7 +219,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra
} }
//MARK: - PageMoleculeTransformationBehavior //MARK: - PageMoleculeTransformationBehavior
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject) accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
} }

View File

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

View File

@ -73,7 +73,7 @@ public class AlertOperation: MVMCoreOperation {
if await !self.properties.getIsDisplayed() { if await !self.properties.getIsDisplayed() {
self.markAsFinished() self.markAsFinished()
} else { } else {
(CoreUIObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject) (MVMCoreObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject)
if self.isCancelled { if self.isCancelled {
await self.dismissAlertView() await self.dismissAlertView()
} }

View File

@ -294,8 +294,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
let classMoleculeB = moleculeB as? NSObjectProtocol { let classMoleculeB = moleculeB as? NSObjectProtocol {
return classMoleculeA === classMoleculeB return classMoleculeA === classMoleculeB
} }
// Do json check // ID check
return moleculeA.toJSON() == moleculeB.toJSON() return moleculeA.id == moleculeB.id
} }
} }

View File

@ -8,15 +8,20 @@
import UIKit import UIKit
open class ThreeLayerTableViewController: ProgrammaticTableViewController { open class ThreeLayerTableViewController: ProgrammaticTableViewController, RotorViewElementsProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Main Views // MARK: - Main Views
//-------------------------------------------------- //--------------------------------------------------
private var topView: UIView?
private var bottomView: UIView?
private var headerView: UIView? private var headerView: UIView?
private var footerView: UIView? private var footerView: UIView?
public var topView: UIView?
public var middleView: UIView? {
get { tableView }
set { }
}
public var bottomView: UIView?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties

View File

@ -9,7 +9,12 @@
import UIKit import UIKit
import MVMCore 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 alertHandler: AlertHandler?
public var topNotificationHandler: NotificationHandler? { public var topNotificationHandler: NotificationHandler? {
didSet { didSet {
@ -17,16 +22,14 @@ import MVMCore
} }
} }
public var accessibilityHandler: AccessibilityHandler? public var accessibilityHandler: AccessibilityHandler?
open override func defaultInitialSetup() { public func defaultInitialSetup() {
MVMCoreObject.sharedInstance()?.defaultInitialSetup()
CoreUIModelMapping.registerObjects() CoreUIModelMapping.registerObjects()
loadHandler = MVMCoreLoadHandler() MVMCoreObject.sharedInstance()?.session = MVMCoreUISession()
cache = MVMCoreCache() MVMCoreObject.sharedInstance()?.actionHandler = MVMCoreUIActionHandler()
session = MVMCoreUISession() MVMCoreObject.sharedInstance()?.viewControllerMapping = MVMCoreUIViewControllerMappingObject()
sessionHandler = MVMCoreSessionTimeHandler() MVMCoreObject.sharedInstance()?.loggingDelegate = MVMCoreUILoggingHandler()
actionHandler = MVMCoreUIActionHandler()
viewControllerMapping = MVMCoreUIViewControllerMappingObject()
loggingDelegate = MVMCoreUILoggingHandler()
alertHandler = AlertHandler() alertHandler = AlertHandler()
accessibilityHandler = AccessibilityHandler() accessibilityHandler = AccessibilityHandler()
} }

View File

@ -9,8 +9,8 @@
#import "MVMCoreUISession.h" #import "MVMCoreUISession.h"
#import "MFLoadingViewController.h" #import "MFLoadingViewController.h"
#import "NSLayoutConstraint+MFConvenience.h" #import "NSLayoutConstraint+MFConvenience.h"
#import <MVMCoreUI/MVMCoreUI-Swift.h> @import MVMCore.MVMCoreLoadingOverlayDelegateProtocol;
@import MVMCore.MVMCoreObject; @import MVMCore.Swift;
@interface MVMCoreUISession () <MVMCoreLoadingOverlayDelegateProtocol> @interface MVMCoreUISession () <MVMCoreLoadingOverlayDelegateProtocol>