Digital PCT265 story PCT-135: Inline replacement updates with the core render loop.
This commit is contained in:
parent
9de1437edc
commit
f37e7abcb1
@ -261,8 +261,9 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra
|
|||||||
accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method.
|
accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method.
|
||||||
}
|
}
|
||||||
|
|
||||||
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? {
|
||||||
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
|
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
|
open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
|
||||||
|
|||||||
@ -145,4 +145,16 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
|||||||
&& indicatorColor == model.indicatorColor
|
&& indicatorColor == model.indicatorColor
|
||||||
&& position == model.position
|
&& position == model.position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool {
|
||||||
|
guard let model = model as? Self else { return false }
|
||||||
|
return backgroundColor == model.backgroundColor
|
||||||
|
&& animated == model.animated
|
||||||
|
&& hidesForSinglePage == model.hidesForSinglePage
|
||||||
|
&& accessibilityHasSlidesInsteadOfPage == model.accessibilityHasSlidesInsteadOfPage
|
||||||
|
&& enabled == model.enabled
|
||||||
|
&& inverted == model.inverted
|
||||||
|
&& disabledIndicatorColor == model.disabledIndicatorColor
|
||||||
|
&& indicatorColor == model.indicatorColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,14 +171,20 @@ open class Carousel: View {
|
|||||||
|
|
||||||
guard let carouselModel = model as? CarouselModel else { return }
|
guard let carouselModel = model as? CarouselModel else { return }
|
||||||
|
|
||||||
|
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)")
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
if let originalModel, carouselModel.isVisuallyEquivalent(to: originalModel) {
|
if let originalModel, carouselModel.isVisuallyEquivalent(to: originalModel) {
|
||||||
collectionView.reconfigureItems(at: collectionView.indexPathsForVisibleItems)
|
// Prevents a carousel reset while still updating the cell backing data through reconfigureItems.
|
||||||
|
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is visually equivalent. Skipping rebuild...")
|
||||||
FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate)
|
FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
|
pagingView?.currentIndex = originalModel.index // Trigger a paging view render.
|
||||||
|
collectionView.reconfigureItems(at: collectionView.indexPathsForVisibleItems)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is new. Rebuilding carousel.")
|
||||||
accessibilityLabel = carouselModel.accessibilityText
|
accessibilityLabel = carouselModel.accessibilityText
|
||||||
collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor
|
collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor
|
||||||
collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0
|
collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0
|
||||||
|
|||||||
@ -228,3 +228,11 @@ extension CarouselModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CarouselModel: CustomDebugStringConvertible {
|
||||||
|
|
||||||
|
public var debugDescription: String {
|
||||||
|
return "\(molecules.count) \(molecules.map { ($0 as? CarouselItemModel)?.molecule.moleculeName ?? "unknown" } )"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -126,14 +126,16 @@ extension ParentModelProtocol {
|
|||||||
|
|
||||||
public typealias DeepCompareResult = (matched: Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?)
|
public typealias DeepCompareResult = (matched: Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?)
|
||||||
|
|
||||||
|
public typealias ModelPair = (mine: ModelProtocol, theirs: ModelProtocol)
|
||||||
|
|
||||||
public func deepEquals(to model: any ModelProtocol) -> DeepCompareResult {
|
public func deepEquals(to model: any ModelProtocol) -> DeepCompareResult {
|
||||||
guard let model = model as? ParentModelProtocol else { return (false, self, model) }
|
guard let model = model as? ParentModelProtocol else { return (false, self, model) }
|
||||||
return deepCompare(model) { $0.isEqual(to: $1) }
|
return findFirst(in: model) { $0.isEqual(to: $1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func deepCompare(_ anotherParent: ParentModelProtocol, with test: (ModelProtocol, ModelProtocol)->Bool) -> DeepCompareResult {
|
func findFirst(in anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> DeepCompareResult {
|
||||||
|
|
||||||
guard test(self, anotherParent) else { return (false, myChild: self, theirChild: self)}
|
guard test(self, anotherParent) else { return (false, myChild: self, theirChild: anotherParent)}
|
||||||
|
|
||||||
let myChildren = children
|
let myChildren = children
|
||||||
let theirChildren = anotherParent.children
|
let theirChildren = anotherParent.children
|
||||||
@ -141,7 +143,7 @@ extension ParentModelProtocol {
|
|||||||
for index in myChildren.indices {
|
for index in myChildren.indices {
|
||||||
if let myChild = myChildren[index] as? ParentModelProtocol {
|
if let myChild = myChildren[index] as? ParentModelProtocol {
|
||||||
if let theirChild = theirChildren[index] as? ParentModelProtocol {
|
if let theirChild = theirChildren[index] as? ParentModelProtocol {
|
||||||
let result = myChild.deepCompare(theirChild, with: test)
|
let result = myChild.findFirst(in: theirChild, where: test)
|
||||||
guard result.0 else { return result }
|
guard result.0 else { return result }
|
||||||
} else {
|
} else {
|
||||||
return (false, myChild: myChild, theirChild: theirChildren[index])
|
return (false, myChild: myChild, theirChild: theirChildren[index])
|
||||||
@ -153,4 +155,26 @@ extension ParentModelProtocol {
|
|||||||
|
|
||||||
return (true, nil, nil)
|
return (true, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deepCompare(against anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> [ModelPair] {
|
||||||
|
|
||||||
|
guard test(self, anotherParent) else { return [(self, anotherParent)]}
|
||||||
|
|
||||||
|
let myChildren = children
|
||||||
|
let theirChildren = anotherParent.children
|
||||||
|
guard myChildren.count == theirChildren.count else { return [(self, anotherParent)] }
|
||||||
|
|
||||||
|
var allDiffs = [ModelPair]()
|
||||||
|
for index in myChildren.indices {
|
||||||
|
if let myChild = myChildren[index] as? ParentModelProtocol,
|
||||||
|
let theirChild = theirChildren[index] as? ParentModelProtocol {
|
||||||
|
let childDiffs = myChild.deepCompare(against: theirChild, where: test) as [ModelPair]
|
||||||
|
allDiffs.append(contentsOf: childDiffs)
|
||||||
|
} else if !test(myChildren[index], theirChildren[index]) {
|
||||||
|
allDiffs.append((myChildren[index], theirChildren[index]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDiffs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol {
|
public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol {
|
||||||
var template: String { get }
|
var template: String { get }
|
||||||
var rootMolecules: [MoleculeModelProtocol] { get }
|
var rootMolecules: [MoleculeModelProtocol] { get }
|
||||||
|
/// Page rendering ID. Unique betwen JSON parses.
|
||||||
|
var id: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension TemplateModelProtocol {
|
public extension TemplateModelProtocol {
|
||||||
|
|||||||
@ -35,19 +35,26 @@ public extension TemplateProtocol {
|
|||||||
|
|
||||||
public extension TemplateProtocol where Self: PageBehaviorHandlerProtocol, Self: MVMCoreViewControllerProtocol {
|
public extension TemplateProtocol where Self: PageBehaviorHandlerProtocol, Self: MVMCoreViewControllerProtocol {
|
||||||
|
|
||||||
|
func parseTemplate(loadObject: MVMCoreLoadObject) throws -> TemplateModelProtocol {
|
||||||
|
guard let pageJSON = loadObject.pageJSON else {
|
||||||
|
throw MVMCoreError.error(code: ErrorCode.parsingJSON.rawValue, messageToDisplay: "", messageToLog: "Load object is missing its page JSON!")
|
||||||
|
}
|
||||||
|
return try parseTemplate(pageJSON: pageJSON)
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function to do common parsing logic.
|
/// Helper function to do common parsing logic.
|
||||||
func parseTemplate(json: [AnyHashable: Any]?) throws {
|
func parseTemplate(pageJSON: [AnyHashable: Any]) throws -> TemplateModelProtocol {
|
||||||
guard let pageJSON = json else { return }
|
|
||||||
let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject
|
let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject
|
||||||
let data = try JSONSerialization.data(withJSONObject: pageJSON)
|
let data = try JSONSerialization.data(withJSONObject: pageJSON)
|
||||||
let decoder = JSONDecoder.create(with: delegateObject)
|
let decoder = JSONDecoder.create(with: delegateObject)
|
||||||
templateModel = try decodeTemplate(using: decoder, from: data)
|
let templateModel = try decodeTemplate(using: decoder, from: data)
|
||||||
|
|
||||||
// Add additional required behaviors if applicable.
|
// Add additional required behavior models to the template if applicable.
|
||||||
guard var pageBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else { return }
|
guard var templateBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else {
|
||||||
|
return templateModel
|
||||||
|
}
|
||||||
|
templateBehaviorsModel.traverseAndAddRequiredBehaviors()
|
||||||
|
|
||||||
pageBehaviorsModel.traverseAndAddRequiredBehaviors()
|
return templateModel
|
||||||
var behaviorHandler = self
|
|
||||||
behaviorHandler.applyBehaviors(pageBehaviorModel: pageBehaviorsModel, delegateObject: delegateObject)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,14 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
@objcMembers open class BaseTemplateModel: MVMControllerModelProtocol, TabPageModelProtocol {
|
@objcMembers open class BaseTemplateModel: MVMControllerModelProtocol, TabPageModelProtocol {
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
open class var identifier: String { "" }
|
open class var identifier: String { "" }
|
||||||
|
|
||||||
|
public var id: String = UUID().uuidString
|
||||||
|
|
||||||
public var pageType: String
|
public var pageType: String
|
||||||
public var template: String {
|
public var template: String {
|
||||||
// Although this is done in the extension, it is needed for the encoding.
|
// Although this is done in the extension, it is needed for the encoding.
|
||||||
|
|||||||
@ -21,9 +21,8 @@
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
open override func parsePageJSON() throws {
|
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
try parseTemplate(json: loadObject?.pageJSON)
|
return try parseTemplate(loadObject: loadObject)
|
||||||
try super.parsePageJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override var loadObject: MVMCoreLoadObject? {
|
open override var loadObject: MVMCoreLoadObject? {
|
||||||
@ -80,10 +79,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open override func handleNewData() {
|
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
setup()
|
setup()
|
||||||
registerCells()
|
registerCells()
|
||||||
super.handleNewData()
|
super.handleNewData(pageModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||||
|
|||||||
@ -25,8 +25,8 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate {
|
|||||||
try decoder.decode(ModalListPageTemplateModel.self, from: data)
|
try decoder.decode(ModalListPageTemplateModel.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override open func handleNewData() {
|
override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
super.handleNewData()
|
super.handleNewData(pageModel)
|
||||||
|
|
||||||
closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|||||||
@ -23,8 +23,8 @@ open class ModalMoleculeStackTemplate: MoleculeStackTemplate {
|
|||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
override open func handleNewData() {
|
override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
super.handleNewData()
|
super.handleNewData(pageModel)
|
||||||
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ??
|
let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ??
|
||||||
|
|||||||
@ -46,9 +46,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
open override func parsePageJSON() throws {
|
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
try parseTemplate(json: loadObject?.pageJSON)
|
return try parseTemplate(loadObject: loadObject)
|
||||||
try super.parsePageJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For subclassing the model.
|
// For subclassing the model.
|
||||||
@ -86,15 +85,16 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func handleNewData() {
|
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
|
super.handleNewData(pageModel)
|
||||||
setup()
|
setup()
|
||||||
registerWithTable()
|
registerWithTable()
|
||||||
super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||||
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
|
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
|
||||||
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
|
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
|
||||||
|
|
||||||
super.updateUI(for: molecules)
|
super.updateUI(for: molecules)
|
||||||
|
|
||||||
guard let molecules else { return }
|
guard let molecules else { return }
|
||||||
|
|||||||
@ -20,10 +20,10 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
|
|||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
open override func handleNewData() {
|
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
|
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
|
||||||
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
|
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
|
||||||
super.handleNewData()
|
super.handleNewData(pageModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For subclassing the model.
|
// For subclassing the model.
|
||||||
@ -31,9 +31,8 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
|
|||||||
return try decoder.decode(StackPageTemplateModel.self, from: data)
|
return try decoder.decode(StackPageTemplateModel.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func parsePageJSON() throws {
|
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
try parseTemplate(json: loadObject?.pageJSON)
|
return try parseTemplate(loadObject: loadObject)
|
||||||
try super.parsePageJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override var loadObject: MVMCoreLoadObject? {
|
open override var loadObject: MVMCoreLoadObject? {
|
||||||
|
|||||||
@ -14,9 +14,8 @@ import UIKit
|
|||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
open override func parsePageJSON() throws {
|
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
try parseTemplate(json: loadObject?.pageJSON)
|
return try parseTemplate(loadObject: loadObject)
|
||||||
try super.parsePageJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import MVMCore
|
|||||||
public var needsUpdateUI = false
|
public var needsUpdateUI = false
|
||||||
private var observingForResponses: NSObjectProtocol?
|
private var observingForResponses: NSObjectProtocol?
|
||||||
private var initialLoadFinished = false
|
private var initialLoadFinished = false
|
||||||
|
private var isFirstRender = true
|
||||||
public var previousScreenSize = CGSize.zero
|
public var previousScreenSize = CGSize.zero
|
||||||
|
|
||||||
public var selectedField: UIView?
|
public var selectedField: UIView?
|
||||||
@ -83,14 +84,16 @@ import MVMCore
|
|||||||
|
|
||||||
open func modulesToListenFor() -> [String]? {
|
open func modulesToListenFor() -> [String]? {
|
||||||
let requestModules = loadObject?.requestParameters?.allModules() ?? []
|
let requestModules = loadObject?.requestParameters?.allModules() ?? []
|
||||||
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor() } ?? []
|
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor } ?? []
|
||||||
return requestModules + behaviorModules
|
return requestModules + behaviorModules
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc open func responseJSONUpdated(notification: Notification) {
|
@objc open func responseJSONUpdated(notification: Notification) {
|
||||||
// Checks for a page we are listening for.
|
// Checks for a page we are listening for.
|
||||||
var newData = false
|
var hasDataUpdate = false
|
||||||
|
var pageModel: PageModelProtocol? = nil
|
||||||
if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
|
if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
|
||||||
|
let loadObject,
|
||||||
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
|
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
|
||||||
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
|
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
|
||||||
let pageType = page.optionalStringForKey(KeyPageType),
|
let pageType = page.optionalStringForKey(KeyPageType),
|
||||||
@ -99,8 +102,21 @@ import MVMCore
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}) {
|
}) {
|
||||||
newData = true
|
hasDataUpdate = true
|
||||||
loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
|
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.
|
// Checks for modules we are listening for.
|
||||||
@ -108,7 +124,7 @@ import MVMCore
|
|||||||
let modulesListened = modulesToListenFor() {
|
let modulesListened = modulesToListenFor() {
|
||||||
for moduleName in modulesListened {
|
for moduleName in modulesListened {
|
||||||
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
|
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
|
||||||
newData = true
|
hasDataUpdate = true
|
||||||
var currentModules = loadObject?.modulesJSON ?? [:]
|
var currentModules = loadObject?.modulesJSON ?? [:]
|
||||||
currentModules.updateValue(module, forKey: moduleName)
|
currentModules.updateValue(module, forKey: moduleName)
|
||||||
loadObject?.modulesJSON = currentModules
|
loadObject?.modulesJSON = currentModules
|
||||||
@ -116,21 +132,11 @@ import MVMCore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard newData else { return }
|
guard hasDataUpdate else { return }
|
||||||
|
|
||||||
do {
|
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
||||||
// TODO: Parse parsePageJSON modifies the page model on a different thread than
|
self.handleNewData(pageModel)
|
||||||
// the UI update which could cause discrepancies. Parse should return the resulting
|
})
|
||||||
// object and assignment should be synchronized on handleNewData(model: ).
|
|
||||||
try parsePageJSON()
|
|
||||||
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
|
||||||
self.handleNewData()
|
|
||||||
})
|
|
||||||
} 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<MVMCoreErrorObject?>) -> Bool {
|
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
||||||
@ -142,7 +148,12 @@ import MVMCore
|
|||||||
|
|
||||||
// Parse the model for the page.
|
// Parse the model for the page.
|
||||||
do {
|
do {
|
||||||
try parsePageJSON()
|
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 {
|
} catch let parsingError {
|
||||||
// Log all parsing errors and fail load.
|
// Log all parsing errors and fail load.
|
||||||
if let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, causedBy: parsingError) {
|
if let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, causedBy: parsingError) {
|
||||||
@ -182,10 +193,8 @@ import MVMCore
|
|||||||
return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)"
|
return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)"
|
||||||
}
|
}
|
||||||
|
|
||||||
open func parsePageJSON() throws {
|
open func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier {
|
throw MVMCoreError.error(code: ErrorCode.parsingJSON.rawValue, messageToDisplay: "Template needs to define its model!", messageToLog: "Template needs to define its model!")
|
||||||
MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
||||||
@ -222,26 +231,77 @@ import MVMCore
|
|||||||
|
|
||||||
/// Processes any new data. Called after the page is loaded the first time and on response updates for this page, Triggers a render refresh.
|
/// 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
|
@MainActor
|
||||||
open func handleNewData() {
|
open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||||
if model?.navigationBar == 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()
|
let navigationItem = createDefaultLegacyNavigationModel()
|
||||||
model?.navigationBar = navigationItem
|
newPageModel.navigationBar = navigationItem
|
||||||
}
|
}
|
||||||
|
|
||||||
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
|
self.pageModel = newPageModel
|
||||||
behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar)
|
|
||||||
|
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 {
|
if formValidator == nil { // TODO: Can't change form rules?
|
||||||
let rules = model?.formRules
|
let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules
|
||||||
formValidator = FormValidator(rules)
|
formValidator = FormValidator(rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI()
|
self.pageModel = newPageModel
|
||||||
|
|
||||||
// Notify the manager of new data.
|
/// Run through the differences between separate page model trees.
|
||||||
// Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI.
|
var pageUpdatedModels = [MoleculeModelProtocol]()
|
||||||
manager?.newDataReceived?(in: self)
|
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.
|
/// Applies the latest model to the UI.
|
||||||
@ -312,7 +372,7 @@ import MVMCore
|
|||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
// Do any additional setup after loading the view.
|
// Do any additional setup after loading the view.
|
||||||
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)")
|
debugLog("View Controller Loaded")
|
||||||
|
|
||||||
// We use our own margins.
|
// We use our own margins.
|
||||||
viewRespectsSystemMinimumLayoutMargins = false
|
viewRespectsSystemMinimumLayoutMargins = false
|
||||||
@ -326,7 +386,7 @@ import MVMCore
|
|||||||
initialLoad()
|
initialLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewData()
|
handleNewData(pageModel) // Set outside shouldFinishProcessingLoad.
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func viewDidLayoutSubviews() {
|
open override func viewDidLayoutSubviews() {
|
||||||
@ -395,7 +455,7 @@ import MVMCore
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
stopObservingForResponseJSONUpdates()
|
stopObservingForResponseJSONUpdates()
|
||||||
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)")
|
debugLog("Deallocated")
|
||||||
}
|
}
|
||||||
|
|
||||||
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
@ -514,22 +574,22 @@ import MVMCore
|
|||||||
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
|
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
|
||||||
|
|
||||||
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) {
|
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) {
|
||||||
pageUpdateQueue.addOperation {
|
pageUpdateQueue.addOperation { [self] in
|
||||||
let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in
|
let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in
|
||||||
guard let replacedMolecule = self.attemptToReplace(with: model) else {
|
guard let replacedMolecule = attemptToReplace(with: model) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return (model, replacedMolecule)
|
return (model, replacedMolecule)
|
||||||
}
|
}
|
||||||
let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in
|
let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in
|
||||||
guard !new.isEqual(to: existing) else {
|
guard !new.isEqual(to: existing) else {
|
||||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("UI for molecules: \(new) is the same. Skip UI update.")
|
debugLog("UI for molecules: \(new) is the same. Skip UI update.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
if uiUpdatedModels.count > 0 {
|
if uiUpdatedModels.count > 0 {
|
||||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("Updating UI for molecules: \(uiUpdatedModels)")
|
debugLog("Updating UI for molecules: \(uiUpdatedModels)")
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
self.updateUI(for: uiUpdatedModels)
|
self.updateUI(for: uiUpdatedModels)
|
||||||
}
|
}
|
||||||
@ -668,3 +728,15 @@ import MVMCore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ViewController: CoreLogging {
|
||||||
|
|
||||||
|
public var loggingPrefix: String {
|
||||||
|
"\(self) \(pageType ?? ""): "
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var loggingCategory: String? {
|
||||||
|
return "Rendering"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTran
|
|||||||
model.refreshInterval + lastRefresh.timeIntervalSinceNow // timeIntervalSinceNow in negative since earlier recording (--)
|
model.refreshInterval + lastRefresh.timeIntervalSinceNow // timeIntervalSinceNow in negative since earlier recording (--)
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstTimeLoad = true
|
var firstTimeLoad = true // TODO: Model replacement is probably going to impact this. Need to transfer first load state.
|
||||||
|
|
||||||
var refreshOnShown: Bool {
|
var refreshOnShown: Bool {
|
||||||
if model.refreshOnFirstLoad && firstTimeLoad {
|
if model.refreshOnFirstLoad && firstTimeLoad {
|
||||||
@ -84,11 +84,12 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTran
|
|||||||
Self.debugLog("Initializing for \(model)")
|
Self.debugLog("Initializing for \(model)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onPageNew(rootMolecules: [any MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
public func onPageNew(rootMolecules: [any MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? {
|
||||||
if let behaviorVC = delegateObject?.moleculeDelegate as? ViewController, MVMCoreUIUtility.getCurrentVisibleController() == behaviorVC {
|
if let behaviorVC = delegateObject?.moleculeDelegate as? ViewController, MVMCoreUIUtility.getCurrentVisibleController() == behaviorVC {
|
||||||
// If behavior is initialized after the page is shown, we need to start the timer. Don't immediately start an action. That is triggered by onPageShown if its a fresh view.
|
// If behavior is initialized after the page is shown, we need to start the timer. Don't immediately start an action. That is triggered by onPageShown if its a fresh view.
|
||||||
resumePollingTimer(withRemainingTime: remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval)
|
resumePollingTimer(withRemainingTime: remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
|
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol {
|
|||||||
/// Should the behavior persist regardless of page behavior model updates.
|
/// Should the behavior persist regardless of page behavior model updates.
|
||||||
var transcendsPageUpdates: Bool { get }
|
var transcendsPageUpdates: Bool { get }
|
||||||
|
|
||||||
func modulesToListenFor() -> [String]
|
var modulesToListenFor: [String] { get }
|
||||||
|
|
||||||
/// Initializes the behavior with the model
|
/// Initializes the behavior with the model
|
||||||
init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?)
|
init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?)
|
||||||
@ -22,7 +22,7 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol {
|
|||||||
|
|
||||||
public extension PageBehaviorProtocol {
|
public extension PageBehaviorProtocol {
|
||||||
var transcendsPageUpdates: Bool { return false }
|
var transcendsPageUpdates: Bool { return false }
|
||||||
func modulesToListenFor() -> [String] { return [] }
|
var modulesToListenFor: [String] { return [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,7 +30,7 @@ public extension PageBehaviorProtocol {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol {
|
public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol {
|
||||||
func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?)
|
func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]?
|
||||||
func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?)
|
func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?)
|
||||||
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol)
|
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol)
|
||||||
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar)
|
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar)
|
||||||
@ -41,7 +41,7 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol {
|
|||||||
|
|
||||||
public extension PageMoleculeTransformationBehavior {
|
public extension PageMoleculeTransformationBehavior {
|
||||||
// All optional.
|
// All optional.
|
||||||
func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {}
|
func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { return nil }
|
||||||
func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?) {}
|
func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?) {}
|
||||||
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {}
|
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {}
|
||||||
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) {}
|
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) {}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol {
|
|||||||
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging {
|
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging {
|
||||||
|
|
||||||
public var loggingPrefix: String {
|
public var loggingPrefix: String {
|
||||||
"\(self) \(ObjectIdentifier(self))\n\(moleculeIds)\n"
|
"\(self) \(ObjectIdentifier(self).hashValue) \(moleculeIds.prefix(3)) \(moleculeIds.count > 3 ? "+ \(moleculeIds.count - 3) more" : ""):\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var loggingCategory: String? {
|
public static var loggingCategory: String? {
|
||||||
@ -26,37 +26,22 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moleculeIds: [String]
|
var moleculeIds: [String]
|
||||||
var modulesToListenFor: [String]
|
public var modulesToListenFor: [String]
|
||||||
private var observingForResponses: NSObjectProtocol?
|
private var observingForResponses: NSObjectProtocol?
|
||||||
private var delegateObject: MVMCoreUIDelegateObject?
|
private var delegateObject: MVMCoreUIDelegateObject?
|
||||||
|
|
||||||
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
|
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
|
||||||
moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds
|
moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds
|
||||||
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
|
modulesToListenFor = moleculeIds
|
||||||
if shouldListenForListUpdates {
|
|
||||||
modulesToListenFor = []
|
|
||||||
listenForModuleUpdates()
|
|
||||||
} else {
|
|
||||||
modulesToListenFor = moleculeIds
|
|
||||||
stopListeningForModuleUpdates()
|
|
||||||
}
|
|
||||||
self.delegateObject = delegateObject
|
self.delegateObject = delegateObject
|
||||||
guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return }
|
guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return }
|
||||||
MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType)
|
MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType)
|
||||||
Self.debugLog("Initializing for \((model as! ReplaceableMoleculeBehaviorModel).moleculeIds)")
|
Self.debugLog("Initializing for \((model as! ReplaceableMoleculeBehaviorModel).moleculeIds)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? {
|
||||||
debugLog("onPageNew")
|
|
||||||
self.delegateObject = delegateObject
|
self.delegateObject = delegateObject
|
||||||
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
|
modulesToListenFor = moleculeIds
|
||||||
if shouldListenForListUpdates {
|
|
||||||
modulesToListenFor = []
|
|
||||||
listenForModuleUpdates()
|
|
||||||
} else {
|
|
||||||
modulesToListenFor = moleculeIds
|
|
||||||
stopListeningForModuleUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
let moleculeModels = moleculeIds.compactMap { moleculeId in
|
let moleculeModels = moleculeIds.compactMap { moleculeId in
|
||||||
do {
|
do {
|
||||||
@ -70,65 +55,61 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if moleculeModels.count > 0 {
|
|
||||||
// TODO: Getting dropped into the page update queue. Can we get this replaced without an async dispatch to avoid an animation?
|
return findAndReplace(moleculeModels, in: rootMolecules)
|
||||||
delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels, completionHandler: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func listenForModuleUpdates() {
|
fileprivate func findAndReplace(_ moleculeModels: [any MoleculeModelProtocol], in rootMolecules: [any MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? {
|
||||||
guard observingForResponses == nil else { return }
|
debugLog("onPageNew replacing \(moleculeModels.map { $0.id })")
|
||||||
let pageUpdateQueue = OperationQueue()
|
var hasReplacement = false
|
||||||
pageUpdateQueue.maxConcurrentOperationCount = 1
|
let updatedRootMolecules = rootMolecules.map { rootMolecule in
|
||||||
pageUpdateQueue.qualityOfService = .userInteractive
|
|
||||||
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in
|
// Top level check to return a new root molecule.
|
||||||
self?.responseJSONUpdated(notification: notification)
|
if let updatedMolecule = moleculeModels.first(where: { rootMolecule.id == $0.id }) {
|
||||||
}
|
guard !updatedMolecule.isEqual(to: rootMolecule) else {
|
||||||
}
|
debugLog("onPageNew molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...")
|
||||||
|
return rootMolecule
|
||||||
private func stopListeningForModuleUpdates() {
|
|
||||||
guard let observingForResponses = observingForResponses else { return }
|
|
||||||
NotificationCenter.default.removeObserver(observingForResponses)
|
|
||||||
self.observingForResponses = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func responseJSONUpdated(notification: Notification) {
|
|
||||||
guard let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) else { return }
|
|
||||||
let modules: [MoleculeModelProtocol] = moleculeIds.compactMap { moleculeId in
|
|
||||||
guard let json = modulesLoaded.optionalDictionaryForKey(moleculeId) else { return nil }
|
|
||||||
do {
|
|
||||||
return try convertToModel(moduleJSON: json)
|
|
||||||
} catch {
|
|
||||||
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
|
|
||||||
if let error = error as? HumanReadableDecodingErrorProtocol {
|
|
||||||
coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)"
|
|
||||||
}
|
}
|
||||||
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
|
debugLog("onPageNew replacing \(rootMolecule) with \(updatedMolecule)")
|
||||||
return nil
|
logUpdated(molecule: updatedMolecule)
|
||||||
|
hasReplacement = true
|
||||||
|
return updatedMolecule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep child check to replace a root's child.
|
||||||
|
guard var parentMolecule = rootMolecule as? ParentMoleculeModelProtocol else { return rootMolecule }
|
||||||
|
|
||||||
|
moleculeModels.forEach { newMolecule in
|
||||||
|
do {
|
||||||
|
if let replacedMolecule = try parentMolecule.replaceChildMolecule(with: newMolecule) {
|
||||||
|
guard !replacedMolecule.isEqual(to: newMolecule) else {
|
||||||
|
// Note: Slight risk here of replacing the something in the original tree and misreporting that is it not replaced based on equality.
|
||||||
|
debugLog("onPageNew molecule \(newMolecule) is the same as \(replacedMolecule). skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debugLog("onPageNew replacing \(replacedMolecule) with \(newMolecule)")
|
||||||
|
logUpdated(molecule: newMolecule)
|
||||||
|
hasReplacement = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentMolecule
|
||||||
}
|
}
|
||||||
guard modules.count > 0 else { return }
|
return hasReplacement ? updatedRootMolecules : nil
|
||||||
#if LOGGING
|
|
||||||
let requestParams = (notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters
|
|
||||||
debugLog("Replacing \(modules.map { $0.id }) from \(requestParams?.url?.absoluteString ?? "unknown"), e2eId: \(requestParams?.identifier ?? "unknown")")
|
|
||||||
#endif
|
|
||||||
delegateObject?.moleculeDelegate?.replaceMoleculeData(modules) { replacedModels in
|
|
||||||
let modules = replacedModels.compactMap { modulesLoaded.dictionaryForKey($0.id) }
|
|
||||||
guard let viewController = self.delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return }
|
|
||||||
modules.forEach { MVMCoreUILoggingHandler.shared()?.defaultLogPageUpdate(forController: viewController, from: $0) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func convertToModel(moduleJSON: [String: Any]) throws -> MoleculeModelProtocol {
|
private func logUpdated(molecule: MoleculeModelProtocol) {
|
||||||
guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName),
|
guard let module: [AnyHashable: Any] = delegateObject?.moleculeDelegate?.getModuleWithName(molecule.id),
|
||||||
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else {
|
let viewController = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else {
|
||||||
throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName))
|
debugLog("Missing the originating module \(molecule.id) creating this molecule!")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as! MoleculeModelProtocol
|
MVMCoreUILoggingHandler.shared()?.defaultLogPageUpdate(forController: viewController, from: module)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
debugLog("deinit")
|
debugLog("deinit")
|
||||||
stopListeningForModuleUpdates()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user