diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 4e04dafc..bb67be60 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ 526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */; }; 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; + 71033C002AB60AEA0038D7A4 /* MVMCoreUISession+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */; }; 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; }; 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; }; @@ -755,6 +756,7 @@ 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnCompareChanges.swift; sourceTree = ""; }; 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethod.swift; sourceTree = ""; }; 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = ""; }; + 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUISession+Extension.swift"; sourceTree = ""; }; 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = ""; }; 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = ""; }; @@ -2277,6 +2279,7 @@ D2B18B912361E65A00A9AEDC /* CoreUIObject.swift */, D29DF27721E7A533003B2FB9 /* MVMCoreUISession.h */, D29DF27821E7A533003B2FB9 /* MVMCoreUISession.m */, + 71033BFE2AB609530038D7A4 /* MVMCoreUISession+Extension.swift */, D29DF27321E79E81003B2FB9 /* MVMCoreUILoggingHandler.h */, D29DF27421E79E81003B2FB9 /* MVMCoreUILoggingHandler.m */, AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */, @@ -3093,6 +3096,7 @@ D29C559025C095210082E7D6 /* Video.swift in Sources */, D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */, AA104B1C24474A76004D2810 /* HeadersH2ButtonsModel.swift in Sources */, + 71033C002AB60AEA0038D7A4 /* MVMCoreUISession+Extension.swift in Sources */, 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */, AAE7270C24AC8B8500A3ED0E /* HeadersH2CaretLinkModel.swift in Sources */, BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */, diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index b476324f..96664c06 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -11,44 +11,13 @@ import Combine import MVMCore import WebKit -public enum AccessibilityNotificationType: String, Codable { - - case controllerChanged, layoutChanged, screenChanged, announcement - - //TODO: - Foucs is shifting to respective element only if we add delay only on new viewcontroller appear. Need to investigate futher. - //https://developer.apple.com/forums/thread/132699, - //https://developer.apple.com/forums/thread/655359 - //By default from iOS 13+ focus is getting shifted to first interactive element inside viewcontroller not to the navigationitem left barbutton item so posting layoutChanged notification with delay to push to leftbarbutton item on new screen push - var delay: Double { - switch self { - case .controllerChanged: - return 1.5 - default: - return 0.0 - } - } - - var accessibilityNotification: UIAccessibility.Notification { - switch self { - case .announcement: - return .announcement - case .screenChanged: - return .screenChanged - case .layoutChanged, .controllerChanged: - return .layoutChanged - } - } -} - -public typealias ArgumentHandler = ((NavigationOperationType?) -> Any?) - public class AccessbilityOperation: MVMCoreOperation { private let argument: Any? - private let notificationType: AccessibilityNotificationType + private let notificationType: UIAccessibility.Notification private var timerSource: DispatchSourceTimer? - public init(notificationType: AccessibilityNotificationType, argument: Any?) { + public init(notificationType: UIAccessibility.Notification, argument: Any?) { self.notificationType = notificationType self.argument = argument } @@ -61,15 +30,15 @@ public class AccessbilityOperation: MVMCoreOperation { timerSource = DispatchSource.makeTimerSource() timerSource?.setEventHandler { Task { @MainActor [weak self] in - if !(self?.isCancelled ?? false), let notification = self?.notificationType.accessibilityNotification { - UIAccessibility.post(notification: notification, argument: self?.argument) - self?.markAsFinished() - } else { + guard let self = self, !self.isCancelled else { self?.stop() + return } + UIAccessibility.post(notification: self.notificationType, argument: self.argument) + self.markAsFinished() } } - timerSource?.schedule(deadline: .now() + notificationType.delay) + timerSource?.schedule(deadline: .now()) timerSource?.activate() } @@ -80,17 +49,16 @@ public class AccessbilityOperation: MVMCoreOperation { } } -public enum NavigationOperationType { case `default`, tab } - open class AccessibilityHandler { public static func shared() -> Self? { guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil } return MVMCoreActionUtility.fatalClassCheck(object: shared) } - public weak var delegate: MVMCoreViewControllerProtocol? + public var previousAccessiblityElement: Any? public var anyCancellable: Set = [] + public weak var delegate: MVMCoreViewControllerProtocol? private var accessibilityOperationQueue: OperationQueue = { let queue = OperationQueue() @@ -100,16 +68,16 @@ open class AccessibilityHandler { private var accessibilityId: String? private var announcementText: String? private var hasTopNotitificationInPage: Bool = false - + public init() { - registerWithNotificationCenter() + registerWithResponseLoaded() registerForPageChanges() registerForFocusChanges() - registerForTopNotificationsChanges() } - - /// Registers with the notification center to know when json is updated. - private func registerWithNotificationCenter() { + + // MARK: - Register with Accessibility Handler listeners + /// Registers with the notification center to know when json is updated and to capture previous accessbility focused id & announcment text + private func registerWithResponseLoaded() { NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .sink { [weak self] notification in self?.accessibilityId = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject)?.pageJSON?.optionalStringForKey("accessibilityId") @@ -117,20 +85,15 @@ open class AccessibilityHandler { }.store(in: &anyCancellable) } - /// Registers to know when pages change. - open func registerForPageChanges() { - MVMCoreNavigationHandler.shared()?.addDelegate(self) - } - private func registerForFocusChanges() { //Since foucs shifted to other elements cancelling existing focus shift notifications if any NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification) - .sink { [weak self] notification in + .sink { [weak self] _ in self?.cancelAllOperations() }.store(in: &anyCancellable) } - - private func registerForTopNotificationsChanges() { + + func registerForTopNotificationsChanges() { NotificationHandler.shared()?.onNotificationWillShow .sink { [weak self] (_, model) in self?.hasTopNotitificationInPage = true @@ -150,6 +113,49 @@ open class AccessibilityHandler { }.store(in: &anyCancellable) } + /// Registers to know when pages change. + open func registerForPageChanges() { + NavigationHandler.shared() + .onNavigation + .sink { [self] (event, operation) in + switch event { + case .willNavigate: + willNavigate(operation) + @unknown default: + break + } + }.store(in: &anyCancellable) + } + + private func willNavigate(_ operation: NavigationOperation) { + previousAccessiblityElement = nil + if let announcementText { + post(notification: .announcement, argument: announcementText) + } + if let subNavManagerController = operation.toNavigationControllerViewControllers?.last as? SubNavManagerController { + delegate = subNavManagerController.getCurrentViewController() as? MVMCoreViewControllerProtocol + } else { + delegate = operation.toNavigationControllerViewControllers?.last as? MVMCoreViewControllerProtocol + } + } + + /*private func didNavigate(_ operation: NavigationOperation) { + guard UIAccessibility.isVoiceOverRunning, + let viewController = operation.toNavigationControllerViewControllers?.last, + canPostAccessbilityNotification(for: viewController) else { return } + delegate = viewController as? MVMCoreViewControllerProtocol + guard let view = operation.toNavigationControllerViewControllers?.last?.view else { return } + view.accessibilityElements = getAccessibilityElementsOnScreen() + if hasTopNotitificationInPage { + previousAccessiblityElement = getFirstFocusedElementOnScreen() + } else { + let accessbilityElement = getUIElementBasedOn(id: accessibilityId) + post(notification: .screenChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) + accessibilityId = nil + } + }*/ + + // MARK: - Accessibility Handler operation events open func capturePreviousFocusElement() { previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) } @@ -166,57 +172,27 @@ open class AccessibilityHandler { accessibilityOperationQueue.cancelAllOperations() } - public func post(notification type: AccessibilityNotificationType, argument: Any? = nil) { + public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) { guard UIAccessibility.isVoiceOverRunning else { return } let accessbilityOperation = AccessbilityOperation(notificationType: type, argument: argument) add(operation: accessbilityOperation) } - //To get first foucs element on the screen + //To get first focus element on the screen open func getFirstFocusedElementOnScreen() -> Any? { (delegate as? UIViewController)?.navigationItem.leftBarButtonItem ?? (delegate as? UIViewController)?.navigationItem.titleView ?? (delegate as? UIViewController)?.navigationController?.navigationBar } //Subclass can decide to trigger Accessibility notification on screen change. open func canPostAccessbilityNotification(for viewController: UIViewController) -> Bool { true } -} - -extension AccessibilityHandler: MVMCorePresentationDelegateProtocol { - open func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { - previousAccessiblityElement = nil - delegate = viewController as? MVMCoreViewControllerProtocol - if let announcementText { - post(notification: .announcement, argument: announcementText) - } - } - - open func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { - guard UIAccessibility.isVoiceOverRunning, - canPostAccessbilityNotification(for: viewController) else { return } - //TODO: - For Tabbar change: adding 1.5 sec delay to shift focus to the top. for Temp fix added to check on childern count. If we have top notification in page on pageLoad, we have postnotification for shifting the focus so in this case we are not posting accessiblity notifcation - if hasTopNotitificationInPage { - previousAccessiblityElement = getFirstFocusedElementOnScreen() - } else { - let accessbilityElement = getAccessbilityFocusedElement() - let operationType: NavigationOperationType = navigationController.children.count == 1 ? .tab : .default //TODO: - need to identify the operationType - post(notification: operationType == .tab ? .controllerChanged : .layoutChanged, argument: accessbilityElement ?? getFirstFocusedElementOnScreen()) - accessibilityId = nil - } - } -} - -extension AccessibilityHandler { - - private func getAccessbilityFocusedElement() -> UIView? { - guard let accessibilityModels: [any AccessibilityElementProtocol] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } - guard !accessibilityModels.isEmpty, - let accessibilityModel = (accessibilityModels.filter { $0.id == accessibilityId }).first as? MoleculeModelProtocol else { - return nil - } + func getPreDefinedFocusedElementIfAny() -> UIView? { + guard let accessibilityId, let models: [any Identifiable] = (delegate?.delegateObject?() as? MVMCoreUIDelegateObject)?.moleculeDelegate?.getRootMolecules().allMoleculesOfType() else { return nil } + guard !models.isEmpty, + let model = (models.filter { ($0.id as? String) == accessibilityId }).first else { return nil } return (delegate as? UIViewController)?.view?.getMoleculeViews { (subView: MoleculeViewProtocol) in - guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.getMoleculeModel() as? (any AccessibilityElementProtocol), - moleculeModel.id == (accessibilityModel as? (any AccessibilityElementProtocol))?.id else { + guard let moleculeModel = (subView as? MoleculeViewModelProtocol)?.moleculeModel as? (any Identifiable), + (moleculeModel.id as? String) == (model.id as? String) else { return false } return true @@ -224,6 +200,44 @@ extension AccessibilityHandler { } } +// MARK: - Accessibility Handler Behaviour +///Accessibility Handler Behaviour to detect page shown and post notification to first interactive element on screen or the pre-defined focused element. +struct AccessibilityHandlerBehaviorModel: PageBehaviorModelProtocol { + + var shouldAllowMultipleInstances = false + static var identifier = "accessibilityHandlerBehaviorModel" +} + +class AccessibilityHandlerBehavior: PageVisibilityBehavior { + + required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { } + + public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { + let viewController = updateAccessibilityViews(delegateObject) + AccessibilityHandler.shared()?.post(notification: .layoutChanged, argument: AccessibilityHandler.shared()?.getPreDefinedFocusedElementIfAny() ?? AccessibilityHandler.shared()?.getFirstFocusedElementOnScreen()) + } + + private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) -> UIViewController? { + var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController] + var viewController: UIViewController? + if let managerController = (delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? SubNavManagerController { + var managerControllerViews = [Any?]() + managerControllerViews.append(managerController.navigationController) + managerControllerViews.append(managerController.tabs) + managerControllerViews.append(contentsOf: managerController.view.subviews) + managerController.view.accessibilityElements = managerControllerViews.compactMap { $0 } + } else if let controller = delegateObject?.moleculeDelegate as? UIViewController { + accessibilityElements.append(controller.navigationController) + accessibilityElements.append(contentsOf: controller.view.subviews.reversed()) + accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar) + controller.view.accessibilityElements = accessibilityElements + viewController = controller + } + return viewController + } +} + +// MARK: - Helpers extension UIView { private func getNestedSubviews() -> [T] { diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 359df799..d84efad6 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -372,7 +372,6 @@ 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 - self.model = model guard let model = model as? ToggleModel else { return } @@ -422,5 +421,5 @@ extension Toggle { extension Toggle: MoleculeViewModelProtocol { - public func getMoleculeModel() -> MoleculeModelProtocol? { model } + public var moleculeModel: MoleculeModelProtocol? { model } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index 3757dee5..fc267cff 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,7 +7,7 @@ // -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, AccessibilityElementProtocol { +public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Identifiable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -32,7 +32,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? - public var id: String //-------------------------------------------------- // MARK: - Keys @@ -56,7 +55,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit case offKnobTintColor case fieldKey case groupName - case id } //-------------------------------------------------- @@ -132,7 +130,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, Accessibilit } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - id = try typeContainer.decode(forKey: .id, default: { UUID().uuidString }()) } public func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 47e0ca07..dbd6b2df 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -104,5 +104,12 @@ public extension ModelRegistry { public protocol MoleculeViewModelProtocol: UIView { - func getMoleculeModel() -> MoleculeModelProtocol? + var moleculeModel: MoleculeModelProtocol? { get } +} + +extension MoleculeViewModelProtocol { + + var moleculeModel: MoleculeModelProtocol? { + get { nil } + } } diff --git a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift index 2a6456dd..6c7ada50 100644 --- a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift +++ b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift @@ -12,8 +12,3 @@ import Foundation /// Should return the argument to use for posting a layout change. func getAccessibilityLayoutChangedArgument() -> Any? } - -public protocol AccessibilityElementProtocol: Identifiable { - - var id: String { get set } -} diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index 7480b3ad..dd82a34f 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -11,7 +11,11 @@ import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { public var alertHandler: AlertHandler? - public var topNotificationHandler: NotificationHandler? + public var topNotificationHandler: NotificationHandler? { + didSet { + accessibilityHandler?.registerForTopNotificationsChanges() + } + } public var accessibilityHandler: AccessibilityHandler? open override func defaultInitialSetup() { @@ -24,5 +28,6 @@ import MVMCore viewControllerMapping = MVMCoreUIViewControllerMappingObject() loggingDelegate = MVMCoreUILoggingHandler() alertHandler = AlertHandler() + accessibilityHandler = AccessibilityHandler() } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift b/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift new file mode 100644 index 00000000..89aae858 --- /dev/null +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession+Extension.swift @@ -0,0 +1,22 @@ +// +// MVMCoreUISession.swift +// MVMCoreUI +// +// Created by Bandaru, Krishna Kishore on 16/09/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objc public extension MVMCoreUISession { + + @objc func applyGlobalMVMCoreUIBehaviors(to viewController: UIViewController) { + + guard var behaviorController = viewController as? PageBehaviorHandlerProtocol, let delegateObject = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() else { return } + + let accessibilityHandlerBehavior = AccessibilityHandlerBehavior(model: AccessibilityHandlerBehaviorModel(), delegateObject: (delegateObject as! MVMCoreUIDelegateObject)) + + let behaviors = behaviorController.behaviors ?? [] + behaviorController.behaviors = behaviors + [accessibilityHandlerBehavior] + } +} diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m index 42a3e769..02ed8df2 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -9,6 +9,7 @@ #import "MVMCoreUISession.h" #import "MFLoadingViewController.h" #import "NSLayoutConstraint+MFConvenience.h" +#import @import MVMCore.MVMCoreObject; @interface MVMCoreUISession () @@ -60,6 +61,7 @@ - (void)applyGlobalBehaviorsToController:(nonnull UIViewController *)viewController { // Allow extending frameworks to apply behaviors to add cross cutting concerns to the base controllers. + [self applyGlobalMVMCoreUIBehaviorsTo:viewController]; } @end