// // ViewController.swift // MVMCoreUI // // Created by Scott Pfeil on 11/5/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit @objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate { @objc public var pageType: String? @objc public var loadObject: MVMCoreLoadObject? public var pageModel: MVMControllerModelProtocol? /// Set if this page is containted in a manager. public var manager: (UIViewController & MVMCoreViewManagerProtocol)? /// A temporary iVar backer for delegateObject() until we change the protocol public lazy var delegateObjectIVar: MVMCoreUIDelegateObject = { return MVMCoreUIDelegateObject.create(withDelegateForAll: self) }() public func delegateObject() -> DelegateObject? { return delegateObjectIVar } public var formValidator: FormValidator? public var needsUpdateUI = false 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() // If the screen is showing, can update the navigation controller. if MVMCoreUIUtility.getCurrentVisibleController() == self.manager ?? self { self.setNavigationController() } }) } 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 ViewController.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 { } open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer) -> Bool { guard let pageType = loadObject?.pageType, var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType), !modulesRequired.isEmpty 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 let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorCritical), messageToLog: modulesRequired.description, code: ErrorCode.requiredModuleNotPresent.rawValue, domain: ErrorDomainNative, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject!)) { error.pointee = errorObject } return false } return true } /// Calls processNewData and then sets the ui to update with updateView open func handleNewDataAndUpdateUI() { handleNewData() needsUpdateUI = true 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() { // TODO: remove legacy. Temporary, convert legacy to navigation model. var navigationModel = pageModel?.navigationItem ?? NavigationItemModel() navigationModel.title = pageModel?.screenHeading navigationModel.showLeftPanelButton = isMasterInitiallyAccessible() navigationModel.showRightPanelButton = isSupportInitiallyAccessible() if /*(self as? MVMCoreUITabBarPageControlViewController) != nil ||*/ manager != nil || loadObject?.requestParameters?.tabWasPressed ?? false == true { navigationModel.line = LineModel(type: .none) } pageModel?.navigationItem = navigationModel if formValidator == nil { let rules = pageModel?.formRules formValidator = FormValidator(rules) } if let backgroundColor = pageModel?.backgroundColor { view.backgroundColor = backgroundColor.uiColor } } // MARK: - Navigation Item (Move to model base) open func setNavigationController() { guard let navigationItemModel = pageModel?.navigationItem, let navigationController = manager?.navigationController ?? navigationController else { MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() return } let viewController = manager ?? self if navigationController == MVMCoreUISplitViewController.main()?.navigationController, navigationController.topViewController == viewController { MVMCoreUISession.sharedGlobal()?.splitViewController?.setupPanels() showBottomProgressBar() } NavigationController.set(navigationController: navigationController, navigationItemModel: navigationItemModel, viewController: viewController) } // Eventually will be moved to server open func isMasterInitiallyAccessible() -> Bool { if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false { return false } return MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false } // Eventually will be moved to server 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 } // Eventually will be moved to separate button in navigation item model 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 } // Eventually will be moved to separate button in navigation item model 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 } // Eventually will be moved to Model 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 /// Called only once in viewDidLoad open func initialLoad() { observeForResponseJSONUpdates() } /// Called on screen size update. open func updateViews() { _ = formValidator?.validate() } override open func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)") // We use our own margins. viewRespectsSystemMinimumLayoutMargins = false // Presents from the bottom. modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? .formSheet : .overCurrentContext // Do some initial loading. if !initialLoadFinished { initialLoadFinished = true initialLoad() } handleNewDataAndUpdateUI() } 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 } // First update should be explicit (hence the zero check) if needsUpdateUI || (previousScreenSize != .zero && 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. Can remove check in the future, see viewControllerReady if manager == nil { setNavigationController() } } 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) { // TODO: This check and set aren't technically needed anymore. The one in viewwillappear should be enough. However, there is a timing issue with the manager where the screen lays out before the menu shows, so the screen grows off the screen. Can fix in the future. if let _ = self.view { setNavigationController() } // Janky way to track current page. 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: delegateObject()) } open func logAction(withActionInformation actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) { MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: self, actionInformation: actionInformation, additionalData: additionalData) } // 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: MoleculeViewProtocol) {} 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: .layoutChanged, argument: textField.inputView) } } } open func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { if textField === selectedField { if UIAccessibility.isVoiceOverRunning { view.accessibilityElements = nil UIAccessibility.post(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 } } }