// // ViewController.swift // MVMCoreUI // // Created by Scott Pfeil on 11/5/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, UITextFieldDelegate, UITextViewDelegate { public var pageType: String? public var loadObject: MVMCoreLoadObject? public var pageModel: MVMControllerModelProtocol? /// Set if this page is containted in a manager. public var manager: (UIViewController & MVMCoreViewManagerProtocol)? public var selfDelegateObject: MVMCoreUIDelegateObject? public func delegateObject() -> DelegateObject? { if selfDelegateObject == nil { selfDelegateObject = MVMCoreUIDelegateObject.create(withDelegateForAll: self) } return selfDelegateObject } public var formValidator: FormValidator? public var needsUpdateUI = true private var observingForResponses = false private var initialLoadFinished = false private var previousScreenSize = CGSize.zero public var selectedField: UIView? /// Checks if the screen width has changed open func screenSizeChanged() -> Bool { return MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) } // MARK: - Response handling open func observeForResponseJSONUpdates() { guard !observingForResponses, (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) else { return } observingForResponses = true NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) } open func stopObservingForResponseJSONUpdates() { guard observingForResponses else { return } NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) observingForResponses = false } open func pagesToListenFor() -> [String]? { guard let pageType = loadObject?.pageType else { return nil } return [pageType] } open func modulesToListenFor() -> [String]? { return loadObject?.requestParameters?.modules as? [String] } @objc open func responseJSONUpdated(notification: Notification) { // Checks for a page we are listening for. var newData = false if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap), let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), let pageType = page.optionalStringForKey(KeyPageType), pageType == pageTypeListened else { return false } return true }) { newData = true loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) } // Checks for modules we are listening for. if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap), let modulesListened = modulesToListenFor() { for moduleName in modulesListened { if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { newData = true var currentModules = loadObject?.modulesJSON ?? [:] currentModules.updateValue(module, forKey: moduleName) loadObject?.modulesJSON = currentModules } } } guard newData else { return } do { try parsePageJSON() MVMCoreDispatchUtility.performBlock(onMainThread: { self.handleNewDataAndUpdateUI() }) } catch { if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") { MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) } } } open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer) -> Bool { pageType = loadObject.pageType self.loadObject = loadObject // Verifies all modules needed are loaded. TODO: change to ViewController guard MFViewController.verifyRequiredModulesLoaded(for: loadObject, error: error) else { return false } // Parse the model for the page. do { try parsePageJSON() return true } catch let parsingError { // Log all parsing errors and fail load. if let errorObject = MVMCoreErrorObject.createErrorObject(for: parsingError, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject)) { errorObject.messageToDisplay = MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess) error.pointee = errorObject } return false } } open func parsePageJSON() throws { // Convert legacy to navigation model. if pageModel?.navigationItem == nil { let navigationModel = NavigationItemModel() if navigationModel.title == nil { navigationModel.title = pageModel?.screenHeading } if navigationModel.showLeftPanelButton == nil { navigationModel.showLeftPanelButton = isMasterInitiallyAccessible() } if navigationModel.showRightPanelButton == nil { navigationModel.showRightPanelButton = isSupportInitiallyAccessible() } pageModel?.navigationItem = navigationModel } if self.formValidator == nil { let rules = pageModel?.formRules self.formValidator = FormValidator(rules) } } open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: inout MVMCoreErrorObject?) -> Bool { guard let pageType = loadObject?.pageType, var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType) else { return true } guard let loadedModules = loadObject?.modulesJSON else { return false } for case let key as String in Array(loadedModules.keys) { guard modulesRequired.count > 0 else { break } if let index = modulesRequired.firstIndex(where: {($0 as? String) == key}) { modulesRequired.remove(at: index) } } guard modulesRequired.count == 0 else { if error != nil { error = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorCritical), messageToLog: modulesRequired.description, code: ErrorCode.requiredModuleNotPresent.rawValue, domain: ErrorDomainNative, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject!)) } return false } return true } /// Calls processNewData and then sets the ui to update with updateView open func handleNewDataAndUpdateUI() { handleNewData() self.needsUpdateUI = true self.view.setNeedsLayout() } /// Processes any new data. Called after the page is loaded the first time and on response updates for this page, open func handleNewData() { formValidator?.validate() } // MARK: - Navigation Item (Move to model base) open func set(navigationController: UINavigationController?) { guard let navigationItemModel = pageModel?.navigationItem, let navigationController = navigationController else { MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() return } navigationItem.title = navigationItemModel.title navigationItem.accessibilityLabel = navigationItemModel.title navigationController.setNavigationBarHidden(navigationItemModel.hidden, animated: true) UIColor.setBackgroundColor(forNavigationBar: navigationItemModel.backgroundColor?.uiColor ?? .white, navigationBar: navigationController.navigationBar, transparent: navigationItemModel.transparent) let tint = navigationItemModel.tintColor.uiColor navigationController.navigationBar.tintColor = tint // Have the navigation title match the tint color navigationController.navigationBar.titleTextAttributes?.updateValue(tint, forKey: .foregroundColor) // Update icons if main navigation controller. if navigationController == MVMCoreUISplitViewController.main()?.navigationController, MVMCoreUISplitViewController.main()?.getCurrentVisibleController() == self { MVMCoreUISession.sharedGlobal()?.splitViewController?.setupPanels() MVMCoreUISplitViewController.main()?.setLeftPanelIsAccessible(navigationItemModel.showLeftPanelButton ?? false, for: self) MVMCoreUISplitViewController.main()?.setRightPanelIsAccessible(navigationItemModel.showRightPanelButton ?? false, for: self) showBottomProgressBar() MVMCoreUISession.sharedGlobal()?.splitViewController?.setNavigationIconColor(tint) // Update separator. MVMCoreUISession.sharedGlobal()?.navigationController?.separatorView?.isHidden = /*self is MVMCoreUITabBarPageControlViewController ||*/ manager != nil || loadObject?.requestParameters?.tabWasPressed ?? false == true } } open func isMasterInitiallyAccessible() -> Bool { if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false { return false } return MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false } open func isSupportInitiallyAccessible() -> Bool { if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false { return false } return (MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false) || showRightPanelForScreenBeforeLaunchApp() } open func showRightPanelForScreenBeforeLaunchApp() -> Bool { return loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false } open func isOverridingRightButton() -> Bool { guard let rightPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("rightPanelButtonLink") else { return false } MVMCoreActionHandler.shared()?.handleAction(with: rightPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } open func isOverridingLeftButton() -> Bool { guard let leftPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("leftPanelButtonLink") else { return false } MVMCoreActionHandler.shared()?.handleAction(with: leftPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } open func showBottomProgressBar() { if MVMCoreUISplitViewController.main()?.getCurrentVisibleController() == self, let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent), let progress = Float(progressString) { MVMCoreUISplitViewController.main()?.setBottomProgressBarProgress(progress / Float(100)) } } // MARK: - View lifecycle open func initialLoad() { observeForResponseJSONUpdates() } open func updateViews() { } override open func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)") viewRespectsSystemMinimumLayoutMargins = false // Presents from the bottom. modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? UIModalPresentationStyle.formSheet : UIModalPresentationStyle.overCurrentContext // Do some initial loading. if !initialLoadFinished { initialLoadFinished = true initialLoad() } // Handle data on load handleNewData() view.setNeedsLayout() } open override func viewDidLayoutSubviews() { // Add to fix a constraint bug where the width is zero and things get messed up. guard isViewLoaded, view.bounds.width > 1 else { super.viewDidLayoutSubviews() return } if needsUpdateUI || screenSizeChanged() { updateViews() needsUpdateUI = false } previousScreenSize = view.bounds.size; super.viewDidLayoutSubviews() } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Update the navigation bar ui when view is appearing unless in a manager. The manager is expected to handle. if manager == nil { set(navigationController: navigationController) } } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if manager == nil { MVMCoreUISession.sharedGlobal()?.currentPageType = pageType MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self) } } deinit { stopObservingForResponseJSONUpdates() MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)") } open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait } open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) // Updates the detail view width coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in }) { (UIViewControllerTransitionCoordinatorContext) in self.view.setNeedsLayout() } } // MARK: - MVMCoreViewManagerViewControllerProtocol open func viewControllerReady(inManager manager: UIViewController & MVMCoreViewManagerProtocol) { if initialLoadFinished { set(navigationController: manager.navigationController) } MVMCoreUISession.sharedGlobal()?.currentPageType = pageType MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self) } // MARK: - MVMCoreActionDelegateProtocol open func handleOpenPage(for requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable : Any]?, additionalData: [AnyHashable : Any]?) { formValidator?.addFormParams(requestParameters: requestParameters) requestParameters.parentPageType = loadObject?.pageJSON?.optionalStringForKey("parentPageType") MVMCoreActionHandler.defaultHandleOpenPage(for: requestParameters, additionalData: additionalData, delegateObject: selfDelegateObject) } // MARK: - MoleculeDelegateProtocol open func getModuleWithName(_ name: String?) -> [AnyHashable : Any]? { guard let name = name else { return nil } return loadObject?.modulesJSON?.optionalDictionaryForKey(name) } open func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? { guard let moduleJSON = loadObject?.modulesJSON?.optionalDictionaryForKey(moleculeName), let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else { return nil } do { return try modelType.decode(jsonDict: moduleJSON) as? MoleculeModelProtocol } catch { MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "error: \(error)") } return nil } // Needed otherwise when subclassed, the extension gets called. open func moleculeLayoutUpdated(_ molecule: UIView & MVMCoreUIMoleculeViewProtocol) {} open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { return nil } open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation) {} open func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation) {} // MARK: - UITextFieldDelegate (Check if this is still needed) // To Remove TextFields Bug: Keyboard is not dismissing after reaching textfield max length limit open func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } open func textFieldDidBeginEditing(_ textField: UITextField) { selectedField = textField // TODO: Make this into a protocol if UIAccessibility.isVoiceOverRunning { if let toolBar = textField.inputAccessoryView as? UIToolbar, let _ = toolBar.items?.last, let pickerView = textField.inputView as? UIPickerView { view.accessibilityElements = [pickerView, toolBar] } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: textField.inputView) } } } open func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { if textField === selectedField { if UIAccessibility.isVoiceOverRunning { view.accessibilityElements = nil UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: textField) } selectedField = nil } } @objc open func dismissFieldInput(sender: Any?) { selectedField?.resignFirstResponder() } // MARK: - UITextViewDelegate (Check if this is still needed) open func textViewDidBeginEditing(_ textView: UITextView) { selectedField = textView } open func textViewDidEndEditing(_ textView: UITextView) { if textView === selectedField { selectedField = nil } } }