mvm_core_ui/MVMCoreUI/BaseControllers/ViewController.swift

751 lines
34 KiB
Swift

//
// ViewController.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 11/5/19.
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
import UIKit
import MVMCore
import Combine
@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: AnyCancellable?
private var initialLoadFinished = false
public 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
//--------------------------------------------------
typealias PageUpdateBatch = (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)
open func observeForResponseJSONUpdates() {
guard observingForResponses == nil,
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
else { return }
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
.receive(on: self.pageUpdateQueue) // Background serial queue.
// Receive new data for this page and filter out any that do not apply.
.compactMap { [weak self] notification in
self?.pullUpdates(from: notification) ?? nil
}
// Merge all page and module updates into one update event.
.scan(((nil,nil,nil), 0)) { (accumulator: (PageUpdateBatch, Int), next: PageUpdateBatch ) in
let (accumulatedUpdates, batchCount) = accumulator
// Always take the latest page and the latest modules with same key.
let updatedBatch: PageUpdateBatch = (
next.pageUpdates ?? accumulatedUpdates.pageUpdates,
next.pageModel ?? accumulatedUpdates.pageModel,
next.moduleUpdates?.mergingRight(accumulatedUpdates.moduleUpdates ?? [:])
)
let updates = (updatedBatch, batchCount + 1)
return updates
}
// Delay allowing the previous model update to settle before triggering a re-render.
.throttle(for: .seconds(0.5), scheduler: RunLoop.main, latest: true)
.sink { [weak self] (pendingUpdates: PageUpdateBatch, batchCount: Int) in
guard let self = self else { return }
let (pageUpdates, pageModel, moduleUpdates) = pendingUpdates
if let pageUpdates, pageModel != nil {
self.loadObject?.pageJSON = pageUpdates
}
let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
self.loadObject?.modulesJSON = mergedModuleUpdates
self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page. (Batches: \(batchCount))")
self.handleNewData(pageModel)
}
}
open func stopObservingForResponseJSONUpdates() {
guard let observingForResponses = observingForResponses else { return }
NotificationCenter.default.removeObserver(observingForResponses)
self.observingForResponses = nil
}
func pullUpdates(from notification: Notification) -> PageUpdateBatch? {
// Get the page data.
let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
// Convert the page data into a new model.
var pageModel: PageModelProtocol? = nil
if let pageUpdates {
do {
// TODO: Rewiring to parse from plain JSON rather than this protocol indirection.
pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates)
} catch {
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") {
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
}
}
}
// Get the module data.
let moduleUpdates = extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:])
debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")")
guard (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) else { return nil }
// Bundle the transformations.
return (pageUpdates, pageModel, moduleUpdates)
}
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
}
private func extractInterestedPageType(from pageMap: [String: Any]) -> [String: Any]? {
guard let pageType = pagesToListenFor()?.first(where: { pageTypeListened -> Bool in
guard let page = pageMap.optionalDictionaryForKey(pageTypeListened),
let pageType = page.optionalStringForKey(KeyPageType),
pageType == pageTypeListened
else { return false }
return true
}) else { return nil }
return pageMap.optionalDictionaryForKey(pageType)
}
private func extractInterestedModules(from moduleMap: [String: Any]) -> [String: Any]? {
guard let modulesListened = modulesToListenFor() else { return nil }
return moduleMap.filter { (key: String, value: Any) in
modulesListened.contains { $0 == key }
}
}
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> 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.
// Needed for PageMoleculeTransformationBehavior + PageLocalDataShareBehavior behaviors.
if let behaviorContainer = template as? (PageBehaviorContainerModelProtocol & TemplateModelProtocol) {
var behaviorHandler = self
behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer)
}
isFirstRender = true // Assuming this is only on the first page load from the handler. Might need to revist later.
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<MVMCoreErrorObject?>) -> 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 unless specified otherwise.
@MainActor
open func handleNewData(_ pageModel: PageModelProtocol? = nil, shouldTriggerRender: Bool = true) {
guard var newPageModel = pageModel ?? self.pageModel else { return }
let originalModel = self.pageModel as? MVMControllerModelProtocol
// Refresh our behaviors if there is a page change. Originally set up in shouldFinishProcessingLoad.
if let behaviorContainer = newPageModel as? (PageBehaviorContainerModelProtocol & TemplateModelProtocol),
(originalModel == nil || originalModel!.id != behaviorContainer.id) {
var behaviorHandler = self
behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer)
}
// Setup the default navigation bar if it is missing.
if newPageModel.navigationBar == nil {
let navigationItem = createDefaultLegacyNavigationModel()
newPageModel.navigationBar = navigationItem
}
// Make the template available for onPageNew behavior handling. See if we can have behaviors rely on roots later.
self.pageModel = newPageModel
// Run through behavior tranformations.
var behaviorUpdatedModels = [MoleculeModelProtocol]()
if var newTemplateModel = newPageModel as? TemplateModelProtocol {
behaviorUpdatedModels = runBehaviorTransformations(on: &newTemplateModel)
}
// Apply the form validator to the controller.
if formValidator == nil { // TODO: Can't change form rules?
let rules = (newPageModel as? FormHolderModelProtocol)?.formRules
formValidator = FormValidator(rules)
}
// Reset after tranformations.
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 {
// This isn't ready yet. handleNewData for the ListTemplate triggers a row item reset and full rebuild + StackTemplate isn't targeting individual refreshes anyway.
// pageUpdatedModels = originalModel.findAllTheirsNotEqual(against: newPageModel)
// debugLog("Page molecule updates\n\(pageUpdatedModels)")
isFirstRender = true // Instead force a full render whenever there is a page data change.
}
let allUpdatedMolecules = behaviorUpdatedModels //+ pageUpdatedModels
// Notify the manager of new data.
manager?.newDataReceived?(in: self)
guard shouldTriggerRender else { return }
// Dispatch to decouple execution. First massage data through template classes, then render.
Task { @MainActor in
if allUpdatedMolecules.isEmpty || isFirstRender {
debugLog("Performing full page render...")
updateUI()
} else {
debugLog("Performing partial render of \(allUpdatedMolecules) molecules...")
updateUI(for: allUpdatedMolecules)
}
}
}
/// Applies the latest model to the UI.
open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
isFirstRender = false
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
behavior.willRender(rootMolecules: molecules ?? getRootMolecules(), delegateObjectIVar)
}
guard molecules == nil else { return }
if let backgroundColor = model?.backgroundColor {
view.backgroundColor = backgroundColor.uiColor
}
needsUpdateUI = true
view.setNeedsLayout()
}
func runBehaviorTransformations(on newTemplateModel: inout any TemplateModelProtocol) -> [any MoleculeModelProtocol] {
var behaviorUpdatedModels = [MoleculeModelProtocol]()
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
var changes = [any MoleculeModelProtocol]()
if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar, changes: &changes) {
updatedMolecules.forEach { molecule in
// Replace again in case there is a template level child.
if let _ = try? newTemplateModel.replaceChildMolecule(with: molecule) {
// Only recognize the molecules that actually changed.
if changes.count > 0 {
debugLog("\(behavior) updated \(changes) in template model.")
changes = changes.filter({ model in
!behaviorUpdatedModels.contains { $0.id == model.id }
})
behaviorUpdatedModels.append(contentsOf: changes)
}
} else {
debugLog("Failed to replace \(molecule) in the template model.")
}
}
}
}
return behaviorUpdatedModels
}
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, shouldTriggerRender: false) // Set outside shouldFinishProcessingLoad.
updateUI() // Force the rendering on the same main UI thread.
}
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) { }
//--------------------------------------------------
// 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"
}
}