// // ViewController.swift // MVMCoreUI // // Created by Scott Pfeil on 11/5/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit import MVMCore @objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @objc public var pageType: String? @objc public var loadObject: MVMCoreLoadObject? public var model: MVMControllerModelProtocol? public var pageModel: PageModelProtocol? { get { model } set { model = newValue as? MVMControllerModelProtocol } } /// Set if this page is containted in a manager. public weak var manager: (UIViewController & MVMCoreViewManagerProtocol)? /// A temporary iVar backer for delegateObject() until we change the protocol open lazy var delegateObjectIVar: MVMCoreUIDelegateObject = { MVMCoreUIDelegateObject.create(withDelegateForAll: self) }() public func delegateObject() -> DelegateObject? { delegateObjectIVar } public var formValidator: FormValidator? public var behaviors: [PageBehaviorProtocol]? public var needsUpdateUI = false private var observingForResponses: NSObjectProtocol? private var initialLoadFinished = false private var isFirstRender = true public var previousScreenSize = CGSize.zero public var selectedField: UIView? public var pageUpdateQueue: OperationQueue = { let pageUpdateQueue = OperationQueue() pageUpdateQueue.maxConcurrentOperationCount = 1 pageUpdateQueue.qualityOfService = .userInteractive return pageUpdateQueue }() /// Checks if the screen width has changed open func screenSizeChanged() -> Bool { !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) } //-------------------------------------------------- // MARK: - Response handling //-------------------------------------------------- open func observeForResponseJSONUpdates() { guard observingForResponses == nil, (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) else { return } observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in self?.responseJSONUpdated(notification: notification) } } open func stopObservingForResponseJSONUpdates() { guard let observingForResponses = observingForResponses else { return } NotificationCenter.default.removeObserver(observingForResponses) self.observingForResponses = nil } open func pagesToListenFor() -> [String]? { guard let pageType = loadObject?.pageType else { return nil } return [pageType] } open func modulesToListenFor() -> [String]? { let requestModules = loadObject?.requestParameters?.allModules() ?? [] let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor } ?? [] return requestModules + behaviorModules } @objc open func responseJSONUpdated(notification: Notification) { // Checks for a page we are listening for. var hasDataUpdate = false var pageModel: PageModelProtocol? = nil if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap), let loadObject, 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 }) { hasDataUpdate = true loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) // TODO: Parse parsePageJSON modifies the page model on a different thread than // the UI update which could cause discrepancies. Parse should return the resulting // object and assignment should be synchronized on handleNewData(model: ). // Separate page updates from the module updates to avoid unecessary resets to behaviors and full re-renders. do { pageModel = try parsePageJSON(loadObject: loadObject) } catch { if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") { MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) } } } // 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) { hasDataUpdate = true var currentModules = loadObject?.modulesJSON ?? [:] currentModules.updateValue(module, forKey: moduleName) loadObject?.modulesJSON = currentModules } } } guard hasDataUpdate else { return } MVMCoreDispatchUtility.performBlock(onMainThread: { self.handleNewData(pageModel) }) } open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer) -> Bool { pageType = loadObject.pageType self.loadObject = loadObject // Verifies all modules needed are loaded. guard ViewController.verifyRequiredModulesLoaded(for: loadObject, error: error) else { return false } // Parse the model for the page. do { let template = try parsePageJSON(loadObject: loadObject) pageModel = template // TODO: Eventually this page parsing should be done outside of this class and then set by the caller. For now, double duty. isFirstRender = true if let backgroundRequest = loadObject.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject.identifier { MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil)) } } catch let parsingError { // Log all parsing errors and fail load. if let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, causedBy: parsingError) { errorObject.messageToDisplay = MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess) errorObject.messageToLog = describe(parsingError: parsingError) error.pointee = errorObject } return false } if let pageData = loadObject.dataForPage { executeBehaviors { (behavior: PageLocalDataShareBehavior) in behavior.receiveLocalPageData(pageData, delegateObjectIVar) } } ///Check with behavior if it can continue do{ let allFinishedProcessingLoad = try executeThrowingBehaviors {(behavior: PageMoleculeTransformationBehavior) in return try behavior.shouldFinishProcessingLoad(loadObject) } guard allFinishedProcessingLoad else { return false } } catch let behaviorError { if let errorObject = MVMCoreErrorObject.createErrorObject(for: behaviorError, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject)) { error.pointee = errorObject } return false } return true } func describe(parsingError: Error) -> String { if let error = parsingError as? HumanReadableDecodingErrorProtocol { return "Error parsing template. \(error.readableDescription)" } return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)" } open func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol { throw MVMCoreError.error(code: ErrorCode.parsingJSON.rawValue, messageToDisplay: "Template needs to define its model!", messageToLog: "Template needs to define its model!") } 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 loadObject = loadObject, let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, withTitle:nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorCritical), code: ErrorCode.requiredModuleNotPresent.rawValue, domain: ErrorDomainNative) { errorObject.messageToLog = modulesRequired.description error.pointee = errorObject } return false } return true } /// Creates a legacy navigation model. open func createDefaultLegacyNavigationModel() -> NavigationItemModel { let navigationModel = NavigationItemModel() navigationModel.title = model?.screenHeading return navigationModel } /// Processes any new data. Called after the page is loaded the first time and on response updates for this page, Triggers a render refresh. @MainActor open func handleNewData(_ pageModel: PageModelProtocol? = nil) { guard var newPageModel = pageModel ?? self.pageModel else { return } let originalModel = self.isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil if originalModel != nil, let behaviorContainer = newPageModel as? PageBehaviorContainerModelProtocol { var behaviorHandler = self behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer) } if newPageModel.navigationBar == nil { let navigationItem = createDefaultLegacyNavigationModel() newPageModel.navigationBar = navigationItem } self.pageModel = newPageModel var behaviorUpdatedModels = [MoleculeModelProtocol]() if var newTemplateModel = newPageModel as? TemplateModelProtocol { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar) { updatedMolecules.forEach { molecule in if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { if !replaced.isEqual(to: molecule) { // Only recognize the molecules that actually changed. debugLog("Behavior updated \(molecule) in template model.") behaviorUpdatedModels.append(molecule) // Need to specifically trace molecule updates here as replacements are modifying the original tree. (We don't have a deep copy.) } } } } } } if formValidator == nil { // TODO: Can't change form rules? let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules formValidator = FormValidator(rules) } self.pageModel = newPageModel /// Run through the differences between separate page model trees. var pageUpdatedModels = [MoleculeModelProtocol]() if let originalModel, // We had a prior. let newPageModel = newPageModel as? TemplateModelProtocol, originalModel.id != newPageModel.id { let diffs = newPageModel.deepCompare(against: originalModel) { new, old in !new.isEqual(to: old) } debugLog("Page molecule updates\n\(diffs.map {"\($0.mine) vs. \($0.theirs)"}.joined(separator: "\n"))") pageUpdatedModels = diffs.compactMap { $0.mine as? MoleculeModelProtocol } } let allUpdatedMolecules = isFirstRender ? [] : behaviorUpdatedModels + pageUpdatedModels isFirstRender = false // Dispatch to decouple execution. First massage data through template classes, then render. Task { @MainActor in if allUpdatedMolecules.isEmpty { debugLog("Performing full page render...") updateUI() } else { debugLog("Updating \(allUpdatedMolecules) molecules...") updateUI(for: allUpdatedMolecules) } // Notify the manager of new data. // Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI. manager?.newDataReceived?(in: self) } } /// Applies the latest model to the UI. open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { guard molecules == nil else { return } executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.willRender(rootMolecules: getRootMolecules(), delegateObjectIVar) } if let backgroundColor = model?.backgroundColor { view.backgroundColor = backgroundColor.uiColor } needsUpdateUI = true view.setNeedsLayout() } public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.willSetupMolecule(with: model, updating: nil) } guard let moleculeView = ModelRegistry.createMolecule(model, delegateObject: delegateObjectIVar) else { return nil } executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.didSetupMolecule(view: moleculeView, withModel: model) } return moleculeView } public func updateMoleculeView(_ view: MoleculeViewProtocol, from model: MoleculeModelProtocol) { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.willSetupMolecule(with: model, updating: view) } view.set(with: model, delegateObjectIVar, nil) executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.didSetupMolecule(view: view, withModel: model) } } //-------------------------------------------------- // MARK: - Navigation Item //-------------------------------------------------- open func getNavigationModel() -> NavigationItemModelProtocol? { // TODO: remove legacy. Temporary, convert legacy to navigation model. if model?.navigationBar == nil { let navigationItem = createDefaultLegacyNavigationModel() model?.navigationBar = navigationItem } return model?.navigationBar } //-------------------------------------------------- // 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. debugLog("View Controller Loaded") // 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() } handleNewData(pageModel) // Set outside shouldFinishProcessingLoad. } 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()) { needsUpdateUI = false updateViews() } previousScreenSize = view.bounds.size; super.viewDidLayoutSubviews() } open func pageShown() { // Track. MVMCoreUISession.sharedGlobal()?.currentPageType = pageType MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self) executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in behavior.onPageShown(self?.delegateObjectIVar) } if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier { MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageRenderComplete(pageType: pageType, requestUUID: identifier, templateName: loadObject?.pageJSON?.optionalStringForKey("template"), controllerName: "\(type(of: self))", error: loadObject?.responseInfoMap?.optionalStringForKey("message"))) } } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in behavior.willShowPage(self?.delegateObjectIVar) } } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if manager == nil { pageShown() } } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in behavior.willHidePage(self?.delegateObjectIVar) } } open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in behavior.onPageHidden(self?.delegateObjectIVar) } } deinit { stopObservingForResponseJSONUpdates() debugLog("Deallocated") } open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 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) { pageShown() } //-------------------------------------------------- // MARK: - MVMCoreLoadDelegateProtocol //-------------------------------------------------- // TODO: Move this function out of here after architecture cleanup. open func loadFinished(_ loadObject: MVMCoreLoadObject?, loadedViewController: (UIViewController & MVMCoreViewControllerProtocol)?, error: MVMCoreErrorObject?) { MVMCoreUILoggingHandler.log(withDelegateLoadFinished: loadObject, loadedViewController: loadedViewController, error: error) // Open the support panel if error == nil, (loadObject?.requestParameters?.openSupportPanel ?? false) || (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) { MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true) } } /// Override this method to avoid adding form params. open func addFormParams(requestParameters: MVMCoreRequestParameters, additionalData: [AnyHashable: Any]?) { formValidator?.addFormParams(requestParameters: requestParameters, model: MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? MoleculeModelProtocol & FormItemProtocol) } public func handleFieldErrors(_ fieldErrors: [Any]?, loadObject: MVMCoreLoadObject) { for case let fieldError as [AnyHashable: Any] in fieldErrors ?? [] { guard let fieldName = fieldError["fieldName"] as? String, let userError = fieldError["userMessage"] as? String, let entryFieldModel = formValidator?.fields[fieldName] as? EntryFieldModel else { continue } entryFieldModel.shouldClearText = fieldError["clearText"] as? Bool ?? true entryFieldModel.dynamicErrorMessage = userError } } //-------------------------------------------------- // MARK: - MVMCoreActionDelegateProtocol //-------------------------------------------------- open func logAction(withActionInformation actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) { MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: self, actionInformation: actionInformation, additionalData: additionalData) } open func performAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) async throws { var additionalData = additionalData if let model = model as? ActionOpenPageModel { addFormParams(requestParameters: model.requestParameters, additionalData: additionalData) model.requestParameters.parentPageType = loadObject?.pageJSON?.optionalStringForKey("parentPageType") var pageForwardedData = additionalData ?? [:] executeBehaviors { (behavior: PageLocalDataShareBehavior) in let dataMap = behavior.compileLocalPageDataForTransfer(delegateObjectIVar) pageForwardedData.merge(dataMap) { current, _ in current } } additionalData = pageForwardedData } do { if let behavior = behaviors?.compactMap({ $0 as? PageCustomActionHandlerBehavior }).first(where: { $0.canHandleAction(with: model, additionalData: additionalData) }) { logAction(withActionInformation: model.toJSON(), additionalData: additionalData) try await behavior.handleAction(with: model, additionalData: additionalData) } else { try await MVMCoreUIActionHandler.shared()?.handleAction(with: model, additionalData: additionalData, delegateObject: delegateObject) } } catch { handleAction(error: error, model: model, additionalData: additionalData, delegateObject: delegateObject) throw error } } open func handleAction(error: Error, model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) { let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: MVMCoreActionHandler.getErrorLocation(with: delegateObject?.actionDelegate, actionType: model.actionType))! switch (model) { case let model as ActionOpenPageModel: errorObject.silentError = model.background ?? false default: errorObject.silentError = false } MVMCoreUIActionHandler.shared()?.defaultHandleActionError(errorObject, additionalData: additionalData, delegateObject: delegateObject) } //-------------------------------------------------- // MARK: - MoleculeDelegateProtocol //-------------------------------------------------- open func getTemplateModel() -> TemplateModelProtocol? { model } open func getRootMolecules() -> [MoleculeModelProtocol] { model?.rootMolecules ?? [] } // Needed otherwise when subclassed, the extension gets called. open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) { pageUpdateQueue.addOperation { [self] in let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in guard let replacedMolecule = attemptToReplace(with: model) else { return nil } return (model, replacedMolecule) } let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in guard !new.isEqual(to: existing) else { debugLog("UI for molecules: \(new) is the same. Skip UI update.") return nil } return new } if uiUpdatedModels.count > 0 { debugLog("Updating UI for molecules: \(uiUpdatedModels)") DispatchQueue.main.sync { self.updateUI(for: uiUpdatedModels) } } completionHandler?(replacedModels.map { $0.0 }) } } open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> MoleculeModelProtocol? { guard var templateModel = getTemplateModel() else { return nil } var replacedMolecule: MoleculeModelProtocol? do { replacedMolecule = try templateModel.replaceMolecule(with: replacementModel) if replacedMolecule == nil { MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(replacementModel.id)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!) } } catch { let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! if let error = error as? HumanReadableDecodingErrorProtocol { coreError.messageToLog = "Error replacing molecule \"\(replacementModel.id)\": \(error.readableDescription)" } MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) } return replacedMolecule } //-------------------------------------------------- // MARK: - MVMCoreUIDetailViewProtocol //-------------------------------------------------- public func isLeftPanelAccessible() -> Bool { // TODO: Remove when hamburger menu is fully phased out. if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false { return false } return MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false } public func isRightPanelAccessible() -> Bool { // TODO: Remove when FAB is 100%. if let hideRightPanel = model?.hideRightPanel, hideRightPanel == true { return false } return (MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false) || showRightPanelForScreenBeforeLaunchApp() } open func showRightPanelForScreenBeforeLaunchApp() -> Bool { loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false } // TODO: make molecular 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 } // TODO: make molecular 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 bottomProgress() -> Float? { guard let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent), let progress = Float(progressString) else { return nil } return progress / Float(100) } //-------------------------------------------------- // MARK: - UITextFieldDelegate //-------------------------------------------------- // 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 //-------------------------------------------------- public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { selectedField = textView return true } open func textViewDidBeginEditing(_ textView: UITextView) { selectedField = textView } open func textViewDidEndEditing(_ textView: UITextView) { if textView === selectedField { selectedField = nil } } } extension ViewController: CoreLogging { public var loggingPrefix: String { "\(self) \(pageType ?? ""): " } public static var loggingCategory: String? { return "Rendering" } }