diff --git a/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme b/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme
deleted file mode 100644
index 9e236f99..00000000
--- a/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift
new file mode 100644
index 00000000..65ae8272
--- /dev/null
+++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift
@@ -0,0 +1,275 @@
+//
+// AccessibilityHandler.swift
+// MVMCoreUI
+//
+// Created by Bandaru, Krishna Kishore on 30/06/23.
+// Copyright © 2023 Verizon Wireless. All rights reserved.
+//
+
+import Foundation
+import Combine
+import MVMCore
+import WebKit
+
+//MARK: - AccessibilityOperation
+public class AccessibilityOperation: MVMCoreOperation {
+
+ private let argument: Any?
+ private let notificationType: UIAccessibility.Notification
+
+ public init(notificationType: UIAccessibility.Notification, argument: Any?) {
+ self.notificationType = notificationType
+ self.argument = argument
+ }
+
+ /**
+ This method will post accessibility notification.
+ */
+ public override func main() {
+ guard !checkAndHandleForCancellation() else { return }
+ guard UIAccessibility.isVoiceOverRunning else {
+ markAsFinished()
+ return
+ }
+ Task { @MainActor [weak self] in
+ guard let self = self, !self.isCancelled else {
+ self?.markAsFinished()
+ return
+ }
+ UIAccessibility.post(notification: self.notificationType, argument: self.argument)
+ self.markAsFinished()
+ }
+ }
+}
+
+//MARK: - AccessibilityHandler
+/**
+ AccessibilityHandler will observe the page visibility of every view controller and post notification to the first interactive element on the screen.
+ If we have to shift/focus custom element on the screen on controller shown then we need to pass accessibilityId in the page response.
+ */
+open class AccessibilityHandler {
+
+ public static func shared() -> Self? {
+ guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil }
+ return MVMCoreActionUtility.fatalClassCheck(object: shared)
+ }
+ //TODO: Revisit to avoid state properties to store in handler.
+ public var accessibilityId: String? ///This property is used to post accessibility to the UIElement mapped to this accessibilityId
+ public var previousAccessiblityElement: Any? ///This property is capture accessiblity element
+ public var anyCancellable: Set = []
+ public weak var currentController: UIViewController? { MVMCoreUIUtility.getCurrentVisibleController() }
+ public var delegateObject: MVMCoreUIDelegateObject?
+ //TODO: Revisit to identify the page has top notification or not.
+ private var hasTopNotificationInPage: Bool { (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil }
+ private let accessibilityOperationQueue: OperationQueue = {
+ let queue = OperationQueue()
+ queue.maxConcurrentOperationCount = 1
+ return queue
+ }()
+
+ lazy var rotorHandler = RotorHandler(accessibilityHandler: self)
+
+ /**
+ init method will register for focus changes
+ */
+ public init() {
+ registerForFocusChanges()
+ }
+
+ /**
+ This method will capture current focused element
+ */
+ open func capturePreviousFocusElement() {
+ previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver)
+ }
+
+ /**
+ This method will post accessibility notification to previous captured element
+ */
+ open func postAccessibilityToPrevElement() {
+ post(notification: .layoutChanged, argument: previousAccessiblityElement)
+ previousAccessiblityElement = nil
+ }
+
+ /**
+ This method will check if voice over is running then will post notification to the mentioned argument
+ */
+ public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) {
+ guard UIAccessibility.isVoiceOverRunning else { return }
+ let AccessibilityOperation = AccessibilityOperation(notificationType: type, argument: argument)
+ accessibilityOperationQueue.addOperation(AccessibilityOperation)
+ }
+
+ /**
+ This method return first focused element from the screen.
+ If navigationBar is hidden then we are returning nil so that voice over will shift to the first interactive element.
+ */
+ open func getFirstFocusedElementOnScreen() -> Any? {
+ ((currentController as? PageProtocol)?.pageModel?.navigationBar?.hidden ?? false) ? nil : currentController?.navigationController?.navigationBar
+ }
+
+ /**
+ This method is used to decide if AccessibilityHandler can post screen change notification or specific classes will take care of posting Accessibility notification
+ */
+ open func canPostAccessibilityNotification(for viewController: UIViewController) -> Bool { true }
+
+ /**
+ This method is used to identify the UIElement that is mapped to accessibilityId from server response.
+ */
+ func getPreDefinedFocusedElementIfAny() -> UIView? {
+ guard let accessibilityId, let view = currentController?.view else { return nil }
+ return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first {
+ $0.model?.id == accessibilityId
+ }
+ }
+}
+
+extension AccessibilityHandler {
+
+ /**
+ This method is used to notify rotor handler about page loaded and capturing the accessibilityId if any.
+ */
+ public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
+ previousAccessiblityElement = nil
+ rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject)
+ guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return }
+ accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId")
+ self.delegateObject = delegateObject
+ }
+
+ /**
+ This method is used to capture accessibility views on the screen.
+ If the page has accessibilityId, then it will not post any accessibility notification because respective UI mapped element can be identified only on page shown.
+ If it has top notification then we are capturing the first focused element and will not post any accessibility notification.
+ If page doesn't have any top notification or accessibilityId then it will post notification to shift focus to first focused element on the screen.
+ */
+ public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
+ updateAccessibilityViews(delegateObject)
+ guard let controller = delegateObject?.moleculeDelegate as? UIViewController,
+ canPostAccessibilityNotification(for: controller),
+ accessibilityId == nil else { return }
+ if hasTopNotificationInPage {
+ previousAccessiblityElement = getFirstFocusedElementOnScreen()
+ } else {
+ post(notification: .layoutChanged, argument: getFirstFocusedElementOnScreen())
+ }
+ }
+
+ /**
+ This method is used to notify rotor handler about page visibility
+ Temp fix: - We are resetting the view accessibilityElements when focus shifted to first focused element on the screen not to have voice over struck in between view elements.
+ https://developer.apple.com/forums/thread/655359
+ https://developer.apple.com/forums/thread/675427
+ If the page has accessibilityId, i.e if server decides to manually shift focus to one of the UIElement then we are identifying the id mapped UIElement & shifting focus to that element.
+ If we have top notification as well in the page then we take that as priority and holding the server driven UIElement in previousAccessiblityElement and post accessibility notification.
+ https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain
+ */
+ public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
+ 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 {
+ previousAccessiblityElement = accessibilityElement
+ } else {
+ post(notification: .layoutChanged, argument: accessibilityElement)
+ }
+ }
+
+ /**
+ This method is used to set view elements as accessibilityElements due to the accessibility behaviour change when new controller is pushed on navigation stack from iOS13+
+ */
+ private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) {
+ 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() {
+ 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: - AccessibilityHandler listeners
+@objc extension AccessibilityHandler {
+
+ /**
+ This method listens for focus changes.
+ When focus is changed manually then we are cancelling existing operations.
+ */
+ 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)
+ }
+
+ /**
+ This method listens for top notification changes.
+ When top notification is about to display it will capture previous focused element.
+ When top notification is displayed it will post notification to that notification view
+ When top notification is about to dismiss then it will post announcement that top alert is closed.
+ When top notification is dimissed then it will post notification back to previously captured element.
+ */
+ 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"))
+ }.store(in: &anyCancellable)
+ NotificationHandler.shared()?.onNotificationDismissed
+ .sink { [weak self] (view, model) in
+ self?.postAccessibilityToPrevElement()
+ }.store(in: &anyCancellable)
+ }
+}
+
+// MARK: - AccessibilityHandlerBehaviour
+/**
+ To notify AccessibilityHandler about the page visibility changes
+ */
+//TODO: Revisit why we need a behavior as a notifier.
+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.
+ }
+
+ open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
+ accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
+ }
+
+ open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
+ accessibilityHandler?.willShowPage(delegateObject)
+ }
+
+ open func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
+ accessibilityHandler?.onPageShown(delegateObject)
+ }
+}
diff --git a/MVMCoreUI/Accessibility/RotorHandler.swift b/MVMCoreUI/Accessibility/RotorHandler.swift
new file mode 100644
index 00000000..a4546980
--- /dev/null
+++ b/MVMCoreUI/Accessibility/RotorHandler.swift
@@ -0,0 +1,357 @@
+//
+// RotorHandler.swift
+// MVMCoreUI
+//
+// Created by Bandaru, Krishna Kishore on 13/10/23.
+// Copyright © 2023 Verizon Wireless. All rights reserved.
+//
+
+import Foundation
+import Combine
+
+//MARK: - RotorType that our app supports
+fileprivate enum RotorType: String, CaseIterable {
+
+ case button = "Buttons"
+ case header = "Header"
+ case link = "Link"
+
+ var trait: UIAccessibilityTraits {
+ switch self {
+ case .button:
+ return .button
+ case .header:
+ return .header
+ case .link:
+ return .link
+ }
+ }
+
+ ///Filter block on model elements based on rotor type
+ var modelFilter: ((MoleculeModelProtocol) -> Bool) {
+ switch self {
+ default:
+ return { $0.accessibilityTraits?.contains(trait) ?? false }
+ }
+ }
+
+ ///Filter block on model UIElements based on rotor type
+ var filter: ((UIView) -> Bool) {
+ switch self {
+ default:
+ return { $0.accessibilityTraits.contains(trait) && !$0.isHidden }
+ }
+ }
+
+ ///Returns custom rotor of the specific type
+ 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
+ }
+}
+
+//MARK: - RotorViewElementsProtocol
+public protocol RotorViewElementsProtocol: MVMCoreViewControllerProtocol {
+ var topView: UIView? { get set }
+ var middleView: UIView? { get set }
+ var bottomView: UIView? { get set }
+}
+
+//MARK: - RotorProtocols
+public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol {
+ func scrollRectToVisible(_ rect: CGRect, animated: Bool)
+}
+
+public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol {
+ func scrollToRow(at indexPath: IndexPath, animated: Bool) -> Void
+ func cellForRow(at indexPath: IndexPath) -> UIView?
+ func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath?
+}
+
+//MARK: - Rotor Element - RotorElement info when we are traversing on model elements in the page.
+struct RotorElement {
+
+ let indexPath: IndexPath?
+ let model: MoleculeModelProtocol
+ let carouselItemModel: MoleculeModelProtocol?///This element is the parent of model item if we have rotor element inside carousel. This is used to scroll to specific carousel item when rotor mode is enabled.
+
+ init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) {
+ self.indexPath = indexPath
+ self.model = model
+ self.carouselItemModel = carouselItemModel
+ }
+}
+
+//MARK: - Rotor Handler
+class RotorHandler {
+
+ private var rotorIndexes: [RotorType: Int] = [:]
+ private var rotorElements: [RotorType: [Any]] = [:]
+ private var anyCancellable: Set = []
+ 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()
+ }
+
+ ///Preparing rotors when accessibility voiceover is turned after page is loaded.
+ private func registerForVoiceOverChanges() {
+ NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
+ .sink { [weak self] _ in
+ guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self?.identifyAndPrepareRotors(self?.delegateObject)
+ }
+ }.store(in: &anyCancellable)
+ }
+
+ //MARK: - Pagevisibility behaviour methods.
+ public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
+ rotorIndexes = [:]
+ rotorElements = [:]
+ self.delegateObject = delegateObject
+ }
+
+ public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
+ identifyAndPrepareRotors(delegateObject)
+ }
+
+ //MARK: - Rotor methods
+ /**
+ This method prepares custom rotors that will be assigned to the current controller.
+ Rotor will be created only if the page contains that trait mapped element or the conditions met.
+ */
+ 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.view.accessibilityCustomRotors = customRotors
+ }
+
+ /**
+ This method prepares trait mapped elements of the current controller and from its manager if exists.
+ */
+ 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 { type.filter($0) })
+ }
+ return accessibilityElements.compactMap { $0 }
+ }
+
+ /**
+ This method prepares trait mapped elements from manager
+ */
+ 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 { type.filter($0) } as [Any]
+ }
+
+ /**
+ This method prepares triat mapped elements from the current viewcontroller.
+ For BAU pages: This will traverse through the view hierarchy for trait mapped elements.
+ For Molecular pages: This will traverse through the models to identify the trait mapped model. Along with traversed models, again we are traversing on view hierarchy if any subViews are added to view to get trait mapped elements.
+ */
+ private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [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 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
+ }
+
+ /**
+ This method is to get rotor elements form Molecular pages only.
+ We are filtering out header, molecules, footer from rootMolcules because rootMolcules may or maynot have the correct order of molecule models(header, body, footer). If we don't maintain order, Voiceover might first go footer then body elements then header.(Safety step)
+ */
+ 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
+ }
+
+ /**
+ This method actually travers through the molecules and identify triat mapped model element along with indexPath of the element if its List/Collection templates.
+ Along with model, indexPath we are also capturing carouselItemModel because when we have Carousel inside the molecule, we need to scroll to carousel item if we have trait mapped rotor element inside the Carousel - (Multi Scroll behaviour)
+ Identiying the CarouselModel, CarouselItemModel by depth.
+ */
+ private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] {
+ var traitIndexPath: IndexPath?
+ 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) {
+ result.append(.init(indexPath: traitIndexPath, model: model, carouselItemModel: carouselItemModel))
+ }
+ return result
+ }
+ return rotorElements as [Any]
+ }
+
+ /**
+ This method creates a rotor based on the RotorType.
+ UIAccessibilityCustomRotor.Search Predicate block is used to return the current rotor UI element
+ If the rotor element is of type UIElement(subclasses of UIView) then it will post the accessibility notification directly to that UI element and scrolling that element to the visible area.
+ If rotor element is of type RotorElement then
+ 1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
+ 2. After the cell view is captured, traversing the cell hierarchy which matches the trait & id of that view's model.
+ 3. After identifying the element, then will post the accessibility notification directly to that UI element
+ If we have carouselItemModel in RotorElement then
+ 1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
+ 2. After cell is identified then we are identifying Carousel from the view hierarchy & scroll to the Carousel item which matches the carouselItemModel.id
+ 3. After carouselItemModel is scrolled then traversing the carouselCellItem hierarchy which matches the trait & id of that view's model
+ 4. After identifying the element, then will post the accessibility notification directly to that UI element
+ */
+ 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] ?? -1
+ switch predicate.searchDirection {
+ case .next:
+ if rotorIndex + 1 < elements.count {
+ rotorIndex += 1
+ }
+ case .previous:
+ if rotorIndex > 0 {
+ rotorIndex -= 1
+ }
+ @unknown default:
+ rotorIndex = 0
+ }
+ guard rotorIndex >= 0, !elements.isEmpty else { return UIAccessibilityCustomRotorItemResult() } //Safety check to avoid crash.
+ var rotorElement = elements[rotorIndex]
+ if let element = rotorElement as? RotorElement,
+ 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)
+ (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)
+ }
+ }
+}
+
+//MARK: - Protocol Extensions
+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 {
+
+ 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, 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 {
+
+ public func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
+ 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/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift
index 85ad8e9c..4341614e 100644
--- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift
+++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift
@@ -372,7 +372,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
-
+
guard let model = model as? ToggleModel else { return }
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift
index 429a5769..f4ce9234 100644
--- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift
+++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift
@@ -77,9 +77,10 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
// MARK: - Initializer
//--------------------------------------------------
- public init(_ state: Bool) {
+ public init(_ state: Bool, id: String = UUID().uuidString) {
self.selected = state
baseValue = state
+ self.id = id
}
//--------------------------------------------------
diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift
index 4cff7bdb..2eefd716 100644
--- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift
+++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift
@@ -213,9 +213,7 @@ open class BarsIndicatorView: CarouselIndicator {
let accessibleIndex = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: index + 1))
else { return }
- let accessibilityValue = String(format: accessibleValueFormat, accessibleIndex, numberOfPages)
- view.accessibilityLabel = accessibilityValue
- view.accessibilityIdentifier = accessibilityValue
+ view.accessibilityLabel = String(format: accessibleValueFormat, accessibleIndex, numberOfPages)
}
public override func assessTouchOf(_ touchPoint_X: CGFloat) {
diff --git a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift
index ca0acd79..9618cfab 100644
--- a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift
+++ b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift
@@ -15,7 +15,7 @@ public enum CheckboxPosition: String, Codable {
case bottom
}
-@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol {
+@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
open class var identifier: String { "checkboxLabel" }
public var moleculeName: String = CheckboxLabelModel.identifier
@DecodableDefault.UUIDString public var id: String
@@ -25,6 +25,7 @@ public enum CheckboxPosition: String, Codable {
public var checkbox: CheckboxModel
public var label: LabelModel
+ public var children: [MoleculeModelProtocol] { [checkbox, label] }
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift
index 01614f31..9af907e1 100644
--- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift
+++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift
@@ -44,6 +44,7 @@ public typealias ActionBlock = () -> ()
public var shouldMaskWhileRecording: Bool = false
+ public var model: MoleculeModelProtocol?
//------------------------------------------------------
// MARK: - Multi-Action Text
//------------------------------------------------------
@@ -408,6 +409,7 @@ public typealias ActionBlock = () -> ()
attributedText = attributedString
originalAttributedString = attributedText
}
+ self.model = labelModel
}
@objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {
diff --git a/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift
index a9fde5c1..a68cdc99 100644
--- a/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift
+++ b/MVMCoreUI/Atomic/Atoms/Views/ProgressBar.swift
@@ -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) {
diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift
index 32c8015a..09f905e7 100644
--- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift
+++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift
@@ -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]?
diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift
index bbe17e05..65b116f5 100644
--- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift
+++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift
@@ -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,31 +74,35 @@ 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)
}
}
// MARK: - TabBarProtocol
+ @MainActor
public func highlightTab(at index: Int) {
- MVMCoreDispatchUtility.performBlock(onMainThread: {
- guard let newSelectedItem = self.items?[index] else { return }
- self.model.selectedTab = index
- self.selectedItem = newSelectedItem
- })
+ guard let items = items, index >= 0, index < items.count else {
+ MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Invalid tab index \(index). \(items?.count ?? 0) tabs available .", code: 0, domain: ErrorDomainSystem, location: #function)!)
+ return
+ }
+ tabModel.selectedTab = index
+ selectedItem = items[index]
}
+ @MainActor
public func selectTab(at index: Int) {
- MVMCoreDispatchUtility.performBlock(onMainThread: {
- guard let newSelectedItem = self.items?[index] else { return }
- self.selectedItem = newSelectedItem
- self.tabBar(self, didSelect: newSelectedItem)
- })
+ guard let items = items, index >= 0, index < items.count else {
+ MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Invalid tab index \(index). \(items?.count ?? 0) tabs available.", code: 0, domain: ErrorDomainSystem, location: #function)!)
+ return
+ }
+ selectedItem = items[index]
+ tabBar(self, didSelect: items[index])
}
- public func currentTabIndex() -> Int { model.selectedTab }
+ public func currentTabIndex() -> Int { tabModel.selectedTab }
}
extension UITabBarItem: MFButtonProtocol { }
diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift
index a5320ad2..15af9686 100644
--- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift
+++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift
@@ -18,6 +18,7 @@
public var hideArrow: Bool?
public var line: LineModel?
public var style: ListItemStyle?
+ public var accessibilityText: String?
//--------------------------------------------------
// MARK: - Keys
@@ -29,6 +30,7 @@
case hideArrow
case line
case style
+ case accessibilityText
}
//--------------------------------------------------
@@ -102,6 +104,7 @@
hideArrow = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideArrow)
line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line)
style = try typeContainer.decodeIfPresent(ListItemStyle.self, forKey: .style)
+ accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
try super.init(from: decoder)
}
@@ -113,5 +116,6 @@
try container.encodeIfPresent(hideArrow, forKey: .hideArrow)
try container.encodeIfPresent(line, forKey: .line)
try container.encodeIfPresent(style, forKey: .style)
+ try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
}
}
diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift
index 4c86ed76..1555c957 100644
--- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift
+++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift
@@ -9,13 +9,15 @@
import Foundation
-open class HeadlineBodyToggleModel: MoleculeModelProtocol {
+open class HeadlineBodyToggleModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
public static var identifier: String = "headlineBodyToggle"
public var moleculeName: String = HeadlineBodyToggleModel.identifier
@DecodableDefault.UUIDString public var id: String
open var backgroundColor: Color?
open var headlineBody: HeadlineBodyModel
open var toggle: ToggleModel
+
+ public var children: [MoleculeModelProtocol] { [headlineBody, toggle] }
public init(_ headlineBody: HeadlineBodyModel, _ toggle: ToggleModel) {
self.headlineBody = headlineBody
diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift
index a25426d7..da5e959e 100644
--- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift
+++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift
@@ -88,15 +88,15 @@
var message = ""
- if let eyebrowLabel = eyebrow.text {
+ if let eyebrowLabel = eyebrow.accessibilityLabel ?? eyebrow.text {
message += eyebrowLabel + ", "
}
- if let headlineLabel = headline.text {
+ if let headlineLabel = headline.accessibilityLabel ?? headline.text {
message += headlineLabel + ", "
}
- if let bodyLabel = body.text {
+ if let bodyLabel = body.accessibilityLabel ?? body.text {
message += bodyLabel
}
diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift
index f2eee9d9..f32d1308 100644
--- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift
+++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift
@@ -376,9 +376,7 @@ open class Carousel: View {
self.carouselAccessibilityElement = carouselAccessibilityElement
}
- if let currentCell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), let pagingView = self.pagingView {
- _accessibilityElements = [currentCell, carouselAccessibilityElement, pagingView]
- } else if let currentCell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
+ if let currentCell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
_accessibilityElements = [currentCell, carouselAccessibilityElement]
} else {
_accessibilityElements = [carouselAccessibilityElement]
diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift
index a7508e39..809334d8 100644
--- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift
+++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift
@@ -12,6 +12,9 @@ import Foundation
public protocol AccessibilityModelProtocol {
var accessibilityIdentifier: String? { get set }
+ var accessibilityTraits: UIAccessibilityTraits? { get set }
+ var accessibilityText: String? { get set }
+ var accessibilityValue: String? { get set }
}
public extension AccessibilityModelProtocol {
@@ -20,4 +23,19 @@ public extension AccessibilityModelProtocol {
get { nil }
set { }
}
+
+ var accessibilityTraits: UIAccessibilityTraits? {
+ get { nil }
+ set { }
+ }
+
+ var accessibilityText: String? {
+ get { nil }
+ set { }
+ }
+
+ var accessibilityValue: String? {
+ get { nil }
+ set { }
+ }
}
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/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift
index 624f99a6..7dd4fa26 100644
--- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift
+++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift
@@ -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)
diff --git a/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift b/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift
index 1ee147f7..b57b7cdd 100644
--- a/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift
+++ b/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift
@@ -13,10 +13,10 @@ import Foundation
var delegateObject: MVMCoreUIDelegateObject? { get set }
/// Should visually select the given tab index.
- @objc func highlightTab(at index: Int)
+ @MainActor func highlightTab(at index: Int)
/// Should select the tab index. As if the user selected it.
- @objc func selectTab(at index: Int)
+ @MainActor func selectTab(at index: Int)
/// Returns the current tab
@objc func currentTabIndex() -> Int
diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift
index 1f8cc49e..b242c64a 100644
--- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift
+++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift
@@ -202,3 +202,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/ModalMoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift
index 656d5086..8f8ca005 100644
--- a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift
+++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift
@@ -35,9 +35,4 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate {
MVMCoreUIActionHandler.performActionUnstructured(with: closeAction, additionalData: nil, delegateObject: self.delegateObject())
})
}
-
- open override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- accessibilityElements = [closeButton as Any, tableView as Any]
- }
}
diff --git a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift
index 22fd7778..bcce25b0 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/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift
index 95448c10..4821847a 100644
--- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift
+++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift
@@ -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
diff --git a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift
index 378dc0bc..837d19c7 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.
diff --git a/MVMCoreUI/Containers/NavigationController/NavigationController.swift b/MVMCoreUI/Containers/NavigationController/NavigationController.swift
index 8f72a792..5c7a9757 100644
--- a/MVMCoreUI/Containers/NavigationController/NavigationController.swift
+++ b/MVMCoreUI/Containers/NavigationController/NavigationController.swift
@@ -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)
diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift
index 20f4449c..55e1071c 100644
--- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift
+++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift
@@ -249,6 +249,11 @@ public extension MVMCoreUISplitViewController {
}
extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol {
+
+ public func getAccessibilityElements() -> [Any]? {
+ nil
+ }
+
public func getCurrentViewController() -> UIViewController? {
navigationController?.getCurrentViewController()
}
diff --git a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift
index 7461c93d..c7f0f869 100644
--- a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift
+++ b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift
@@ -308,6 +308,10 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol,
}
}
+ @objc public func getAccessibilityElements() -> [Any]? {
+ [tabs]
+ }
+
open func newDataReceived(in viewController: UIViewController) {
manager?.newDataReceived?(in: viewController)
hideNavigationBarLine(true)
diff --git a/MVMCoreUI/Notification/NotificationContainerView.swift b/MVMCoreUI/Notification/NotificationContainerView.swift
index 62d05a9d..0fee99e0 100644
--- a/MVMCoreUI/Notification/NotificationContainerView.swift
+++ b/MVMCoreUI/Notification/NotificationContainerView.swift
@@ -25,16 +25,6 @@ public class NotificationContainerView: UIView {
super.init(coder: coder)
setupView()
}
-
- /// Posts a layout change with taking the arguments from the view following the AccessibilityProtocol.
- private func updateAccessibilityForTopAlert(_ view: UIView) {
- // Update accessibility with top alert
- var accessibilityArgument: Any? = view
- if let view = view as? AccessibilityProtocol {
- accessibilityArgument = view.getAccessibilityLayoutChangedArgument()
- }
- UIAccessibility.post(notification: .layoutChanged, argument: accessibilityArgument)
- }
}
extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@@ -56,7 +46,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
self.superview?.layoutIfNeeded()
} completion: { finished in
self.superview?.layoutIfNeeded()
- self.updateAccessibilityForTopAlert(notification)
continuation.resume()
}
}
@@ -64,14 +53,11 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@MainActor
public func hide(notification: UIView) async {
- // accessibility - below line added to notify VI user through voiceover user when the top alert is closed
- UIAccessibility.post(notification: .screenChanged, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed"))
await withCheckedContinuation { continuation in
UIView.animate(withDuration: 0.5) {
self.height.isActive = true
self.superview?.layoutIfNeeded()
} completion: { finished in
- UIAccessibility.post(notification: .layoutChanged, argument: nil)
self.currentNotificationView?.removeFromSuperview()
self.currentNotificationView = nil
continuation.resume()
diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift
index d1fd18f9..131c2287 100644
--- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift
+++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift
@@ -9,14 +9,19 @@
import UIKit
import MVMCore
-@objcMembers
+@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?
+ public var topNotificationHandler: NotificationHandler? {
+ didSet {
+ accessibilityHandler?.registerForTopNotificationsChanges()
+ }
+ }
+ public var accessibilityHandler: AccessibilityHandler?
public func defaultInitialSetup() {
MVMCoreObject.sharedInstance()?.defaultInitialSetup()
@@ -26,5 +31,6 @@ public class CoreUIObject: NSObject {
MVMCoreObject.sharedInstance()?.viewControllerMapping = MVMCoreUIViewControllerMappingObject()
MVMCoreObject.sharedInstance()?.loggingDelegate = MVMCoreUILoggingHandler()
alertHandler = AlertHandler()
+ accessibilityHandler = AccessibilityHandler()
}
}
diff --git a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings
index ba70fc2b..0b736bf9 100644
--- a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings
+++ b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings
@@ -75,7 +75,9 @@
// MARK: Carousel
"MVMCoreUIPageControl_currentpage_index" = "page %@ of %d";
-"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d";
+"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d selected";
+"MVMCoreUIPageControlslides_currentpage_index_accessibilityAnnouncement" = "slide %@ of %d";
+"MVMCoreUIPageControlslides_totalslides" = "Carousel containing %d slides,";
// MARK: Styler
diff --git a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings
index 0c441812..150ac167 100644
--- a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings
+++ b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings
@@ -60,7 +60,10 @@
// Carousel
"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d";
-"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d";
+"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d seleccionado";
+"MVMCoreUIPageControlslides_currentpage_index_accessibilityAnnouncement" = "diapositiva %@ of %d";
+"MVMCoreUIPageControlslides_totalslides" = "Carrusel contiene %d diapositivas,";
+
//Styler
"CountDownDay" = " día";
"CountDownHour" = " hora";
diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift
index 05cd9ae2..2f755491 100644
--- a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift
+++ b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift
@@ -39,7 +39,7 @@ public extension MVMCoreUIUtility {
/// - type: The type you are looking for.
/// - views: The starting array of subviews.
/// - Returns: Will return an array of any view associated with the given type. Will return an empty array of none were found.
- static func findViews(by type: T.Type, views: [UIView]) -> [T] {
+ static func findViews(by type: T.Type, views: [UIView], excludedViews: [UIView] = []) -> [T] {
guard !views.isEmpty else { return [] }
@@ -47,6 +47,9 @@ public extension MVMCoreUIUtility {
var matching = [T]()
for view in views {
+ guard !excludedViews.contains(view) else {
+ continue
+ }
if view is T {
matching.append(view as! T)
}
@@ -54,7 +57,7 @@ public extension MVMCoreUIUtility {
queue.append(contentsOf: view.subviews)
}
- return findViews(by: type, views: queue) + matching
+ return findViews(by: type, views: queue, excludedViews: excludedViews) + matching
}
@MainActor