Digital PCT265 story PCT-135: Inline replacement updates with the core render loop.

This commit is contained in:
Hedden, Kyle Matthew 2024-05-13 14:46:35 -04:00
parent 9de1437edc
commit f37e7abcb1
18 changed files with 266 additions and 152 deletions

View File

@ -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?) {

View File

@ -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
}
}

View File

@ -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

View File

@ -228,3 +228,11 @@ extension CarouselModel {
}
}
extension CarouselModel: CustomDebugStringConvertible {
public var debugDescription: String {
return "\(molecules.count) \(molecules.map { ($0 as? CarouselItemModel)?.molecule.moleculeName ?? "unknown" } )"
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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.

View File

@ -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) {

View File

@ -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 }

View File

@ -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 ??

View File

@ -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 }

View File

@ -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? {

View File

@ -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) {

View File

@ -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"
}
}

View File

@ -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?) {

View File

@ -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) {}

View File

@ -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()
}
}