Added RotorHandler & addressed review comments
This commit is contained in:
parent
57d83b4884
commit
521eaa7c15
@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@ -169,6 +169,7 @@
|
||||
52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; };
|
||||
52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.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 */; };
|
||||
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1457,6 +1459,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */,
|
||||
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */,
|
||||
);
|
||||
path = Accessibility;
|
||||
sourceTree = "<group>";
|
||||
@ -3012,6 +3015,7 @@
|
||||
D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */,
|
||||
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */,
|
||||
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
|
||||
71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */,
|
||||
AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */,
|
||||
8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */,
|
||||
D243859923A16B1800332775 /* Container.swift in Sources */,
|
||||
|
||||
@ -49,49 +49,24 @@ public class AccessbilityOperation: MVMCoreOperation {
|
||||
}
|
||||
|
||||
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? {
|
||||
guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil }
|
||||
return MVMCoreActionUtility.fatalClassCheck(object: shared)
|
||||
}
|
||||
|
||||
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 }
|
||||
public weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol }
|
||||
public weak var delegateObject: MVMCoreUIDelegateObject?
|
||||
private var hasTopNotificationInPage: Bool { delegate?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || delegate?.loadObject??.responseInfoMap?.optionalStringForKey("userMessage") != nil }
|
||||
private let accessibilityOperationQueue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.maxConcurrentOperationCount = 1
|
||||
return queue
|
||||
}()
|
||||
|
||||
lazy var rotorHandler = RotorHandler(accessibilityHandler: self)
|
||||
|
||||
public init() {
|
||||
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 {
|
||||
|
||||
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
rotorIndexes = [:]
|
||||
previousAccessiblityElement = nil
|
||||
rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject)
|
||||
guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return }
|
||||
accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId")
|
||||
if let announcementText = loadObject?.pageJSON?.optionalStringForKey("announcementText") {
|
||||
post(notification: .announcement, argument: announcementText)
|
||||
}
|
||||
delegate = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol
|
||||
self.delegateObject = delegateObject
|
||||
}
|
||||
|
||||
//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.
|
||||
///https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain
|
||||
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
identifyAndPrepareRotors(delegateObject)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
(delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil
|
||||
defer {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
(delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil
|
||||
}
|
||||
}
|
||||
rotorHandler.onPageShown(delegateObject)
|
||||
guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return }
|
||||
accessibilityId = nil
|
||||
if hasTopNotificationInPage {
|
||||
@ -173,115 +155,19 @@ extension AccessibilityHandler {
|
||||
|
||||
//MARK: - Accessibility Methods
|
||||
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]
|
||||
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: manager.view.subviews)
|
||||
currentController = manager
|
||||
} else {
|
||||
accessibilityElements.append(contentsOf: currentController?.view.subviews ?? [])
|
||||
accessibilityElements.append(contentsOf: currentController.view.subviews)
|
||||
}
|
||||
accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar)
|
||||
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)
|
||||
}
|
||||
currentController.view.accessibilityElements = accessibilityElements.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,7 +195,7 @@ extension AccessibilityHandler {
|
||||
}.store(in: &anyCancellable)
|
||||
NotificationHandler.shared()?.onNotificationWillDismiss
|
||||
.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)
|
||||
NotificationHandler.shared()?.onNotificationDismissed
|
||||
.sink { [weak self] (view, model) in
|
||||
@ -333,7 +219,7 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra
|
||||
}
|
||||
|
||||
//MARK: - PageMoleculeTransformationBehavior
|
||||
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
|
||||
}
|
||||
|
||||
|
||||
228
MVMCoreUI/Accessibility/RotorHandler.swift
Normal file
228
MVMCoreUI/Accessibility/RotorHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,7 @@ public class AlertOperation: MVMCoreOperation {
|
||||
if await !self.properties.getIsDisplayed() {
|
||||
self.markAsFinished()
|
||||
} else {
|
||||
(CoreUIObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject)
|
||||
(MVMCoreObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject)
|
||||
if self.isCancelled {
|
||||
await self.dismissAlertView()
|
||||
}
|
||||
|
||||
@ -294,8 +294,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
let classMoleculeB = moleculeB as? NSObjectProtocol {
|
||||
return classMoleculeA === classMoleculeB
|
||||
}
|
||||
// Do json check
|
||||
return moleculeA.toJSON() == moleculeB.toJSON()
|
||||
// ID check
|
||||
return moleculeA.id == moleculeB.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,15 +8,20 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
open class ThreeLayerTableViewController: ProgrammaticTableViewController {
|
||||
open class ThreeLayerTableViewController: ProgrammaticTableViewController, RotorViewElementsProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Main Views
|
||||
//--------------------------------------------------
|
||||
|
||||
private var topView: UIView?
|
||||
private var bottomView: UIView?
|
||||
private var headerView: UIView?
|
||||
private var footerView: UIView?
|
||||
public var topView: UIView?
|
||||
public var middleView: UIView? {
|
||||
get { tableView }
|
||||
set { }
|
||||
}
|
||||
public var bottomView: UIView?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
|
||||
@ -9,7 +9,12 @@
|
||||
import UIKit
|
||||
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 topNotificationHandler: NotificationHandler? {
|
||||
didSet {
|
||||
@ -17,16 +22,14 @@ import MVMCore
|
||||
}
|
||||
}
|
||||
public var accessibilityHandler: AccessibilityHandler?
|
||||
|
||||
open override func defaultInitialSetup() {
|
||||
|
||||
public func defaultInitialSetup() {
|
||||
MVMCoreObject.sharedInstance()?.defaultInitialSetup()
|
||||
CoreUIModelMapping.registerObjects()
|
||||
loadHandler = MVMCoreLoadHandler()
|
||||
cache = MVMCoreCache()
|
||||
session = MVMCoreUISession()
|
||||
sessionHandler = MVMCoreSessionTimeHandler()
|
||||
actionHandler = MVMCoreUIActionHandler()
|
||||
viewControllerMapping = MVMCoreUIViewControllerMappingObject()
|
||||
loggingDelegate = MVMCoreUILoggingHandler()
|
||||
MVMCoreObject.sharedInstance()?.session = MVMCoreUISession()
|
||||
MVMCoreObject.sharedInstance()?.actionHandler = MVMCoreUIActionHandler()
|
||||
MVMCoreObject.sharedInstance()?.viewControllerMapping = MVMCoreUIViewControllerMappingObject()
|
||||
MVMCoreObject.sharedInstance()?.loggingDelegate = MVMCoreUILoggingHandler()
|
||||
alertHandler = AlertHandler()
|
||||
accessibilityHandler = AccessibilityHandler()
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
#import "MVMCoreUISession.h"
|
||||
#import "MFLoadingViewController.h"
|
||||
#import "NSLayoutConstraint+MFConvenience.h"
|
||||
#import <MVMCoreUI/MVMCoreUI-Swift.h>
|
||||
@import MVMCore.MVMCoreObject;
|
||||
@import MVMCore.MVMCoreLoadingOverlayDelegateProtocol;
|
||||
@import MVMCore.Swift;
|
||||
|
||||
@interface MVMCoreUISession () <MVMCoreLoadingOverlayDelegateProtocol>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user