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.
|
||||
}
|
||||
|
||||
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? {
|
||||
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
|
||||
return nil
|
||||
}
|
||||
|
||||
open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
|
||||
@ -145,4 +145,16 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
||||
&& indicatorColor == model.indicatorColor
|
||||
&& 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 }
|
||||
|
||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)")
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
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)
|
||||
pagingView?.currentIndex = originalModel.index // Trigger a paging view render.
|
||||
collectionView.reconfigureItems(at: collectionView.indexPathsForVisibleItems)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is new. Rebuilding carousel.")
|
||||
accessibilityLabel = carouselModel.accessibilityText
|
||||
collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor
|
||||
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 ModelPair = (mine: ModelProtocol, theirs: ModelProtocol)
|
||||
|
||||
public func deepEquals(to model: any ModelProtocol) -> DeepCompareResult {
|
||||
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 theirChildren = anotherParent.children
|
||||
@ -141,7 +143,7 @@ extension ParentModelProtocol {
|
||||
for index in myChildren.indices {
|
||||
if let myChild = myChildren[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 }
|
||||
} else {
|
||||
return (false, myChild: myChild, theirChild: theirChildren[index])
|
||||
@ -153,4 +155,26 @@ extension ParentModelProtocol {
|
||||
|
||||
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 {
|
||||
var template: String { get }
|
||||
var rootMolecules: [MoleculeModelProtocol] { get }
|
||||
/// Page rendering ID. Unique betwen JSON parses.
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
public extension TemplateModelProtocol {
|
||||
|
||||
@ -35,19 +35,26 @@ public extension TemplateProtocol {
|
||||
|
||||
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.
|
||||
func parseTemplate(json: [AnyHashable: Any]?) throws {
|
||||
guard let pageJSON = json else { return }
|
||||
func parseTemplate(pageJSON: [AnyHashable: Any]) throws -> TemplateModelProtocol {
|
||||
let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject
|
||||
let data = try JSONSerialization.data(withJSONObject: pageJSON)
|
||||
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.
|
||||
guard var pageBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else { return }
|
||||
// Add additional required behavior models to the template if applicable.
|
||||
guard var templateBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else {
|
||||
return templateModel
|
||||
}
|
||||
templateBehaviorsModel.traverseAndAddRequiredBehaviors()
|
||||
|
||||
pageBehaviorsModel.traverseAndAddRequiredBehaviors()
|
||||
var behaviorHandler = self
|
||||
behaviorHandler.applyBehaviors(pageBehaviorModel: pageBehaviorsModel, delegateObject: delegateObject)
|
||||
return templateModel
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,14 @@ import Foundation
|
||||
|
||||
|
||||
@objcMembers open class BaseTemplateModel: MVMControllerModelProtocol, TabPageModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
open class var identifier: String { "" }
|
||||
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var pageType: String
|
||||
public var template: String {
|
||||
// Although this is done in the extension, it is needed for the encoding.
|
||||
|
||||
@ -21,9 +21,8 @@
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
open override func parsePageJSON() throws {
|
||||
try parseTemplate(json: loadObject?.pageJSON)
|
||||
try super.parsePageJSON()
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
|
||||
open override var loadObject: MVMCoreLoadObject? {
|
||||
@ -80,10 +79,10 @@
|
||||
}
|
||||
|
||||
|
||||
open override func handleNewData() {
|
||||
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||
setup()
|
||||
registerCells()
|
||||
super.handleNewData()
|
||||
super.handleNewData(pageModel)
|
||||
}
|
||||
|
||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||
|
||||
@ -25,8 +25,8 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate {
|
||||
try decoder.decode(ModalListPageTemplateModel.self, from: data)
|
||||
}
|
||||
|
||||
override open func handleNewData() {
|
||||
super.handleNewData()
|
||||
override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||
super.handleNewData(pageModel)
|
||||
|
||||
closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
@ -23,8 +23,8 @@ open class ModalMoleculeStackTemplate: MoleculeStackTemplate {
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
override open func handleNewData() {
|
||||
super.handleNewData()
|
||||
override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||
super.handleNewData(pageModel)
|
||||
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ??
|
||||
|
||||
@ -46,9 +46,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func parsePageJSON() throws {
|
||||
try parseTemplate(json: loadObject?.pageJSON)
|
||||
try super.parsePageJSON()
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
|
||||
// For subclassing the model.
|
||||
@ -86,15 +85,16 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
return view
|
||||
}
|
||||
|
||||
open override func handleNewData() {
|
||||
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||
super.handleNewData(pageModel)
|
||||
setup()
|
||||
registerWithTable()
|
||||
super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel.
|
||||
}
|
||||
|
||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
|
||||
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
|
||||
|
||||
super.updateUI(for: molecules)
|
||||
|
||||
guard let molecules else { return }
|
||||
|
||||
@ -20,10 +20,10 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func handleNewData() {
|
||||
open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
|
||||
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
|
||||
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
|
||||
super.handleNewData()
|
||||
super.handleNewData(pageModel)
|
||||
}
|
||||
|
||||
// For subclassing the model.
|
||||
@ -31,9 +31,8 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
|
||||
return try decoder.decode(StackPageTemplateModel.self, from: data)
|
||||
}
|
||||
|
||||
open override func parsePageJSON() throws {
|
||||
try parseTemplate(json: loadObject?.pageJSON)
|
||||
try super.parsePageJSON()
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
|
||||
open override var loadObject: MVMCoreLoadObject? {
|
||||
|
||||
@ -14,9 +14,8 @@ import UIKit
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func parsePageJSON() throws {
|
||||
try parseTemplate(json: loadObject?.pageJSON)
|
||||
try super.parsePageJSON()
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
|
||||
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
|
||||
|
||||
@ -40,6 +40,7 @@ import MVMCore
|
||||
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?
|
||||
@ -83,14 +84,16 @@ import MVMCore
|
||||
|
||||
open func modulesToListenFor() -> [String]? {
|
||||
let requestModules = loadObject?.requestParameters?.allModules() ?? []
|
||||
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor() } ?? []
|
||||
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor } ?? []
|
||||
return requestModules + behaviorModules
|
||||
}
|
||||
|
||||
@objc open func responseJSONUpdated(notification: Notification) {
|
||||
// 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),
|
||||
let loadObject,
|
||||
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
|
||||
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
|
||||
let pageType = page.optionalStringForKey(KeyPageType),
|
||||
@ -99,8 +102,21 @@ import MVMCore
|
||||
|
||||
return true
|
||||
}) {
|
||||
newData = true
|
||||
loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
|
||||
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.
|
||||
@ -108,7 +124,7 @@ import MVMCore
|
||||
let modulesListened = modulesToListenFor() {
|
||||
for moduleName in modulesListened {
|
||||
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
|
||||
newData = true
|
||||
hasDataUpdate = true
|
||||
var currentModules = loadObject?.modulesJSON ?? [:]
|
||||
currentModules.updateValue(module, forKey: moduleName)
|
||||
loadObject?.modulesJSON = currentModules
|
||||
@ -116,21 +132,11 @@ import MVMCore
|
||||
}
|
||||
}
|
||||
|
||||
guard newData else { return }
|
||||
guard hasDataUpdate else { return }
|
||||
|
||||
do {
|
||||
// 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: ).
|
||||
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)
|
||||
}
|
||||
}
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
||||
self.handleNewData(pageModel)
|
||||
})
|
||||
}
|
||||
|
||||
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
||||
@ -142,7 +148,12 @@ import MVMCore
|
||||
|
||||
// Parse the model for the page.
|
||||
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 {
|
||||
// Log all parsing errors and fail load.
|
||||
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)"
|
||||
}
|
||||
|
||||
open func parsePageJSON() throws {
|
||||
if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier {
|
||||
MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil))
|
||||
}
|
||||
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 {
|
||||
@ -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.
|
||||
@MainActor
|
||||
open func handleNewData() {
|
||||
if model?.navigationBar == nil {
|
||||
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()
|
||||
model?.navigationBar = navigationItem
|
||||
newPageModel.navigationBar = navigationItem
|
||||
}
|
||||
|
||||
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
|
||||
behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar)
|
||||
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 {
|
||||
let rules = model?.formRules
|
||||
if formValidator == nil { // TODO: Can't change form rules?
|
||||
let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules
|
||||
formValidator = FormValidator(rules)
|
||||
}
|
||||
|
||||
updateUI()
|
||||
self.pageModel = newPageModel
|
||||
|
||||
// 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)
|
||||
/// 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.
|
||||
@ -312,7 +372,7 @@ import MVMCore
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)")
|
||||
debugLog("View Controller Loaded")
|
||||
|
||||
// We use our own margins.
|
||||
viewRespectsSystemMinimumLayoutMargins = false
|
||||
@ -326,7 +386,7 @@ import MVMCore
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
handleNewData()
|
||||
handleNewData(pageModel) // Set outside shouldFinishProcessingLoad.
|
||||
}
|
||||
|
||||
open override func viewDidLayoutSubviews() {
|
||||
@ -395,7 +455,7 @@ import MVMCore
|
||||
|
||||
deinit {
|
||||
stopObservingForResponseJSONUpdates()
|
||||
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)")
|
||||
debugLog("Deallocated")
|
||||
}
|
||||
|
||||
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
@ -514,22 +574,22 @@ import MVMCore
|
||||
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
|
||||
|
||||
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) {
|
||||
pageUpdateQueue.addOperation {
|
||||
pageUpdateQueue.addOperation { [self] 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 (model, replacedMolecule)
|
||||
}
|
||||
let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in
|
||||
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 new
|
||||
}
|
||||
if uiUpdatedModels.count > 0 {
|
||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("Updating UI for molecules: \(uiUpdatedModels)")
|
||||
debugLog("Updating UI for molecules: \(uiUpdatedModels)")
|
||||
DispatchQueue.main.sync {
|
||||
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 (--)
|
||||
}
|
||||
|
||||
var firstTimeLoad = true
|
||||
var firstTimeLoad = true // TODO: Model replacement is probably going to impact this. Need to transfer first load state.
|
||||
|
||||
var refreshOnShown: Bool {
|
||||
if model.refreshOnFirstLoad && firstTimeLoad {
|
||||
@ -84,11 +84,12 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTran
|
||||
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 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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
|
||||
@ -14,7 +14,7 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol {
|
||||
/// Should the behavior persist regardless of page behavior model updates.
|
||||
var transcendsPageUpdates: Bool { get }
|
||||
|
||||
func modulesToListenFor() -> [String]
|
||||
var modulesToListenFor: [String] { get }
|
||||
|
||||
/// Initializes the behavior with the model
|
||||
init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?)
|
||||
@ -22,7 +22,7 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol {
|
||||
|
||||
public extension PageBehaviorProtocol {
|
||||
var transcendsPageUpdates: Bool { return false }
|
||||
func modulesToListenFor() -> [String] { return [] }
|
||||
var modulesToListenFor: [String] { return [] }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,7 +30,7 @@ public extension 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 didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol)
|
||||
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar)
|
||||
@ -41,7 +41,7 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol {
|
||||
|
||||
public extension PageMoleculeTransformationBehavior {
|
||||
// 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 didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {}
|
||||
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) {}
|
||||
|
||||
@ -18,7 +18,7 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol {
|
||||
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging {
|
||||
|
||||
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? {
|
||||
@ -26,37 +26,22 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co
|
||||
}
|
||||
|
||||
var moleculeIds: [String]
|
||||
var modulesToListenFor: [String]
|
||||
public var modulesToListenFor: [String]
|
||||
private var observingForResponses: NSObjectProtocol?
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
|
||||
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds
|
||||
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
|
||||
if shouldListenForListUpdates {
|
||||
modulesToListenFor = []
|
||||
listenForModuleUpdates()
|
||||
} else {
|
||||
modulesToListenFor = moleculeIds
|
||||
stopListeningForModuleUpdates()
|
||||
}
|
||||
modulesToListenFor = moleculeIds
|
||||
self.delegateObject = delegateObject
|
||||
guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return }
|
||||
MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType)
|
||||
Self.debugLog("Initializing for \((model as! ReplaceableMoleculeBehaviorModel).moleculeIds)")
|
||||
}
|
||||
|
||||
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
debugLog("onPageNew")
|
||||
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? {
|
||||
self.delegateObject = delegateObject
|
||||
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
|
||||
if shouldListenForListUpdates {
|
||||
modulesToListenFor = []
|
||||
listenForModuleUpdates()
|
||||
} else {
|
||||
modulesToListenFor = moleculeIds
|
||||
stopListeningForModuleUpdates()
|
||||
}
|
||||
modulesToListenFor = moleculeIds
|
||||
|
||||
let moleculeModels = moleculeIds.compactMap { moleculeId in
|
||||
do {
|
||||
@ -70,65 +55,61 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co
|
||||
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?
|
||||
delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels, completionHandler: nil)
|
||||
}
|
||||
|
||||
return findAndReplace(moleculeModels, in: rootMolecules)
|
||||
}
|
||||
|
||||
private func listenForModuleUpdates() {
|
||||
guard observingForResponses == nil else { return }
|
||||
let pageUpdateQueue = OperationQueue()
|
||||
pageUpdateQueue.maxConcurrentOperationCount = 1
|
||||
pageUpdateQueue.qualityOfService = .userInteractive
|
||||
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in
|
||||
self?.responseJSONUpdated(notification: notification)
|
||||
}
|
||||
}
|
||||
|
||||
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)"
|
||||
fileprivate func findAndReplace(_ moleculeModels: [any MoleculeModelProtocol], in rootMolecules: [any MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? {
|
||||
debugLog("onPageNew replacing \(moleculeModels.map { $0.id })")
|
||||
var hasReplacement = false
|
||||
let updatedRootMolecules = rootMolecules.map { rootMolecule in
|
||||
|
||||
// Top level check to return a new root molecule.
|
||||
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
|
||||
}
|
||||
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
|
||||
return nil
|
||||
debugLog("onPageNew replacing \(rootMolecule) with \(updatedMolecule)")
|
||||
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 }
|
||||
#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) }
|
||||
}
|
||||
return hasReplacement ? updatedRootMolecules : nil
|
||||
}
|
||||
|
||||
private func convertToModel(moduleJSON: [String: Any]) throws -> MoleculeModelProtocol {
|
||||
guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName),
|
||||
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else {
|
||||
throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName))
|
||||
private func logUpdated(molecule: MoleculeModelProtocol) {
|
||||
guard let module: [AnyHashable: Any] = delegateObject?.moleculeDelegate?.getModuleWithName(molecule.id),
|
||||
let viewController = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else {
|
||||
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 {
|
||||
debugLog("deinit")
|
||||
stopListeningForModuleUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user