743 lines
32 KiB
Swift
743 lines
32 KiB
Swift
//
|
|
// 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<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.
|
|
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<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.
|
|
@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"
|
|
}
|
|
|
|
}
|