From 436d4c746bd6af2a4dbc96b6ba94163272882514 Mon Sep 17 00:00:00 2001 From: Krishna Kishore Bandaru Date: Thu, 19 Oct 2023 10:35:27 +0530 Subject: [PATCH] review comments and enhancements in rotorhandler --- .../Accessibility/AccessibilityHandler.swift | 11 +- MVMCoreUI/Accessibility/RotorHandler.swift | 134 +++++++++++++----- .../Protocols/MoleculeListProtocol.swift | 6 + .../Atomic/Templates/CollectionTemplate.swift | 8 ++ .../Templates/MoleculeListTemplate.swift | 9 +- .../ThreeLayerCollectionViewController.swift | 10 +- .../ThreeLayerViewController.swift | 8 +- 7 files changed, 127 insertions(+), 59 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 488ae403..32d6a0f2 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -22,13 +22,13 @@ public class AccessibilityOperation: MVMCoreOperation { } public override func main() { - guard UIAccessibility.isVoiceOverRunning, !checkAndHandleForCancellation() else { - stop() + guard UIAccessibility.isVoiceOverRunning else { + markAsFinished() return } Task { @MainActor [weak self] in guard let self = self, !self.isCancelled else { - self?.stop() + self?.markAsFinished() return } UIAccessibility.post(notification: self.notificationType, argument: self.argument) @@ -41,11 +41,6 @@ public class AccessibilityOperation: MVMCoreOperation { } } } - - public func stop() { - guard isCancelled else { return } - markAsFinished() - } } open class AccessibilityHandler { diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift index 5819b198..3c3fdbce 100644 --- a/MVMCoreUI/Accessibility/RotorHandler.swift +++ b/MVMCoreUI/Accessibility/RotorHandler.swift @@ -67,13 +67,22 @@ public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol { } public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol { - func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) -> Void + func scrollToRow(at indexPath: IndexPath, animated: Bool) -> Void func cellForRow(at indexPath: IndexPath) -> UIView? + func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? } struct RotorElement { - let indexPath: IndexPath + + let indexPath: IndexPath? let model: MoleculeModelProtocol + let carouselItemModel: MoleculeModelProtocol? + + init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) { + self.indexPath = indexPath + self.model = model + self.carouselItemModel = carouselItemModel + } } class RotorHandler { @@ -94,7 +103,9 @@ class RotorHandler { NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) .sink { [weak self] _ in guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return } - self?.identifyAndPrepareRotors(self?.delegateObject) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.identifyAndPrepareRotors(self?.delegateObject) + } }.store(in: &anyCancellable) } @@ -120,7 +131,7 @@ class RotorHandler { customRotors.append(rotor) } } - currentViewController.accessibilityCustomRotors = customRotors + currentViewController.view.accessibilityCustomRotors = customRotors } private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? { @@ -143,40 +154,50 @@ class RotorHandler { } 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 { type.filter($0) } as [Any] + guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & ViewController) else { + return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] //BAU pages } - let topViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.topView].compactMap { $0 }).filter { type.filter($0) } 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 { type.filter($0) } as [Any] - } - } - let bottomViewRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] - let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] - return topViewRotorElements + reusableViewRotorElements + remainingRotorElements + bottomViewRotorElements + let rotorElements = getRotorElements(from: template, type: type) + let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] //Other elements added to view if any. + return rotorElements + remainingRotorElements } - 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] = [] + private func getRotorElements(from template: (MVMCoreViewControllerProtocol & ViewController & RotorViewElementsProtocol)?, type: RotorType) -> [Any] { + guard let templateModel = template?.model as? ThreeLayerModelBase else { return [] } + var rotorElements: [Any] = [] + if let headerView = template?.topView { + rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [headerView].compactMap { $0 }).filter { type.filter($0) }) + } + let molecules = templateModel.rootMolecules.filter { !($0.id == templateModel.header?.id || $0.id == templateModel.footer?.id) } + rotorElements.append(contentsOf: getRotorElements(from: molecules, template: template, type: type)) + if let bottomView = template?.bottomView { + rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [bottomView].compactMap { $0 }).filter { type.filter($0) }) + } + return rotorElements + } + + private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] { 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) { + var carouselItemModel: MoleculeModelProtocol? + var carouselDepth: Int? + let rotorElements: [RotorElement] = molecules.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: []) { result, model, depth in + if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol), + let indexPath = (template as? RotorListTypeDelegateProtocol)?.getIndexPathFor(molecule: listModel) { traitIndexPath = indexPath } + if let _carouselDepth = carouselDepth, depth < _carouselDepth { + carouselDepth = nil + carouselItemModel = nil + } + if model is CarouselModel { carouselDepth = depth } + carouselItemModel = model is CarouselItemModelProtocol ? model : carouselItemModel var result = result - if type.modelFilter(model), let traitIndexPath { - result.append(.init(indexPath: traitIndexPath, model: model)) + if type.modelFilter(model) { + result.append(.init(indexPath: traitIndexPath, model: model, carouselItemModel: carouselItemModel)) } return result } - return rotorElements + return rotorElements as [Any] } private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? { @@ -195,10 +216,28 @@ class RotorHandler { guard rotorIndex >= 0 else { return UIAccessibilityCustomRotorItemResult() } var rotorElement = elements[rotorIndex] 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 { type.filter($0) && $0.model?.id == element.model.id }.first as Any + let controller = self.delegate as? (RotorViewElementsProtocol & ViewController) { + var cellView: UIView? + if let indexPath = element.indexPath, let controller = self.delegate as? (any RotorListTypeDelegateProtocol) { + controller.scrollToRow(at: indexPath, animated: false) + cellView = controller.cellForRow(at: indexPath) + } else { + cellView = controller.view + } + if let cauroselItemModel = element.carouselItemModel { + guard let carousel = MVMCoreUIUtility.findViews(by: Carousel.self, views: [cellView].compactMap { $0 }).first, + let index = (carousel.molecules?.firstIndex { $0.id == cauroselItemModel.id }) else { return UIAccessibilityCustomRotorItemResult() } + let collectionIndexPath = IndexPath(item: index, section: 0) + carousel.collectionView.scrollToItem(at: collectionIndexPath, at: .left, animated: false) + let collectionViewCell = carousel.collectionView.cellForItem(at: collectionIndexPath) + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [collectionViewCell].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any + } else { + rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any + if let viewElement = (rotorElement as? UIView) { + let convertedFrame = viewElement.convert(viewElement.frame, to: controller.view) + (self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false) + } + } } else { if let viewElement = (rotorElement as? UIView) { let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view) @@ -223,19 +262,26 @@ extension RotorScrollDelegateProtocol { } extension RotorListTypeDelegateProtocol { - func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { } - func cellForRow(at indexPath: IndexPath) -> UIView? { nil } + + public func scrollToRow(at indexPath: IndexPath, animated: Bool) { } + public func cellForRow(at indexPath: IndexPath) -> UIView? { nil } + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { 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 scrollToRow(at indexPath: IndexPath, animated: Bool) { + tableView.scrollToRow(at: indexPath, at: .middle, animated: animated) } public func cellForRow(at indexPath: IndexPath) -> UIView? { tableView.cellForRow(at: indexPath) } + + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { + guard let molecule = molecule as? (ListItemModelProtocol & MoleculeModelProtocol) else { return nil } + return (self as? MoleculeListTemplate)?.getIndexPath(for: molecule) + } } extension ThreeLayerViewController: RotorScrollDelegateProtocol { @@ -244,3 +290,19 @@ extension ThreeLayerViewController: RotorScrollDelegateProtocol { scrollView?.scrollRectToVisible(rect, animated: animated) } } + +extension ThreeLayerCollectionViewController: RotorListTypeDelegateProtocol { + + public func scrollToRow(at indexPath: IndexPath, animated: Bool) { + collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated) + } + + public func cellForRow(at indexPath: IndexPath) -> UIView? { + collectionView?.cellForItem(at: indexPath) + } + + public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { + guard let molecule = molecule as? (CollectionItemModelProtocol & MoleculeModelProtocol) else { return nil } + return (self as? MoleculeCollectionListProtocol)?.getIndexPath(for: molecule) + } +} diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift index dfeb7b11..8f4bfee0 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift @@ -37,3 +37,9 @@ public extension MVMCoreUIDelegateObject { return (moleculeDelegate as? MoleculeListProtocol & NSObjectProtocol) } } + +public protocol MoleculeCollectionListProtocol { + + /// Asks the delegate for the index of molecule. + func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath? +} diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index b84792a6..3683e1e9 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -198,3 +198,11 @@ return modules } } + +extension CollectionTemplate: MoleculeCollectionListProtocol { + + public func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { + guard let index = (moleculesInfo?.firstIndex { $0.molecule.id == molecule.id }) else { return nil } + return IndexPath(item: index, section: 0) + } +} diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index a113ef5f..30a9b5c5 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -288,14 +288,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol /// Checks if the two molecules are equal private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool { - // TODO: move this to a better approach, maybe a UUID for each model. - // Do instance check - if let classMoleculeA = moleculeA as? NSObjectProtocol, - let classMoleculeB = moleculeB as? NSObjectProtocol { - return classMoleculeA === classMoleculeB - } - // ID check - return moleculeA.id == moleculeB.id + moleculeA.id == moleculeB.id } } diff --git a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift index 4011f4f8..05a575ae 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift @@ -9,10 +9,14 @@ import Foundation /// A view controller that has three main layers, a header, collection rows, and a footer. The header is added as a supplement header to the first section, and the footer is added as a supplement footer to the last section. This view controller allows for flexible space between the three layers to fit the screeen. -@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout { +@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout, RotorViewElementsProtocol { - private var topView: UIView? - private var bottomView: UIView? + public var topView: UIView? + public var middleView: UIView? { + set {} + get { collectionView } + } + public var bottomView: UIView? private var headerView: ContainerCollectionReusableView? private var footerView: ContainerCollectionReusableView? private let headerID = "header" diff --git a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift index ec8aaaa3..4560aa09 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift @@ -9,12 +9,12 @@ import UIKit -open class ThreeLayerViewController: ProgrammaticScrollViewController { +open class ThreeLayerViewController: ProgrammaticScrollViewController, RotorViewElementsProtocol { // The three main views - var topView: UIView? - var middleView: UIView? - var bottomView: UIView? + public var topView: UIView? + public var middleView: UIView? + public var bottomView: UIView? var useMargins: Bool = false // The top view can be put outside of the scrolling area.