From 750e50d476bfeb68603caf611ad60376577d83ea Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 12 Jun 2024 15:59:43 -0400 Subject: [PATCH 1/3] Digital PCT265 story DE307-731: Support 'gone' configuration for carousel cells. --- MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift | 5 +++++ MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 4 ++-- MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift | 4 ++++ .../Protocols/ModelProtocols/CarouselItemModelProtocol.swift | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index f7b875c1..bbcca3f6 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -24,6 +24,7 @@ public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + public var gone: Bool = false //-------------------------------------------------- // MARK: - Validation @@ -53,6 +54,7 @@ case groupName case enabled case readOnly + case gone } //-------------------------------------------------- @@ -76,6 +78,7 @@ if let readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) { self.readOnly = readOnly } + gone = try typeContainer.decodeIfPresent(Bool.self, forKey: .gone) ?? false try super.init(from: decoder) } @@ -99,6 +102,7 @@ && fieldValue == model.fieldValue && enabled == model.enabled && readOnly == model.readOnly + && gone == model.gone } public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { @@ -107,5 +111,6 @@ && peakingArrowColor == model.peakingArrowColor && enabled == model.enabled && readOnly == model.readOnly + && gone == model.gone } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 12399292..1601acfe 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -90,7 +90,7 @@ open class Carousel: View { showPeaking(false) // Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled. - guard let model = model as? CarouselModel, !model.molecules.isEmpty else { return } + guard let model = model as? CarouselModel, !model.visibleMolecules.isEmpty else { return } guard (model.paging == true || loop == true) else { DispatchQueue.main.async { [self] in updatePagerVisibility() @@ -228,7 +228,7 @@ open class Carousel: View { //-------------------------------------------------- func prepareMolecules(with carouselModel: CarouselModel?) { - guard let newMolecules = carouselModel?.molecules else { + guard let newMolecules = carouselModel?.visibleMolecules else { numberOfPages = 0 molecules = nil return diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 42c8363c..58ee28e6 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -44,6 +44,10 @@ import UIKit public var selectable = false public var selectedIndex: Int? + public var visibleMolecules: [MoleculeModelProtocol & CarouselItemModelProtocol] { + molecules.filter { !$0.gone } + } + public init(molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]) { self.molecules = molecules } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift index ed09d90e..affa294b 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift @@ -9,6 +9,7 @@ public protocol CarouselItemModelProtocol: FormFieldProtocol, ContainerModelProtocol { var analyticsData: JSONValueDictionary? { get set } + var gone: Bool { get set } } public extension CarouselItemModelProtocol { From 7a4984f2d8515f8ba3a51074ece9003d24e835b3 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 12 Jun 2024 23:14:10 -0400 Subject: [PATCH 2/3] Digital PCT265 story DE307-731: Restore missing support for deep replacemenr by moving the template extension to ParentMoleculeModelProtocol. --- .../Atomic/Organisms/Carousel/Carousel.swift | 4 ++- .../CarouselItemModelProtocol.swift | 5 ++++ .../ParentMoleculeModelProtocol.swift | 22 ++++++++++++++- .../TemplateModelProtocol.swift | 28 ------------------- .../ReplaceableMoleculeBehaviorModel.swift | 4 +-- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 1601acfe..df83fc2a 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -174,7 +174,9 @@ open class Carousel: View { 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.isDeeplyVisuallyEquivalent(to: originalModel) { + if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel), + originalModel.visibleMolecules.isVisuallyEquivalent(to: molecules ?? []) // Since the carousel model's children are in place replaced and we do not have a deep copy of this model tree, add in this hack to check if the prior captured carousel items match the newly visible ones. + { // 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) diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift index affa294b..24a0bb02 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/CarouselItemModelProtocol.swift @@ -17,4 +17,9 @@ public extension CarouselItemModelProtocol { get { nil } set { analyticsData = newValue } } + + var gone: Bool{ + get { false } + set { } + } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index d0e1c13f..13bb75b3 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -43,7 +43,7 @@ public extension ParentModelProtocol where Self: AnyObject { func replaceChildMolecule(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { var replacedMolecule: MoleculeModelProtocol? - return try replaceChildMolecule(at: &molecules, with: replacementMolecule, replaced: &replacedMolecule) ? replacedMolecule : nil + return try replaceChildMolecule(in: &molecules, with: replacementMolecule, replaced: &replacedMolecule) ? replacedMolecule : nil } /// Helper for replacing a molecule in place within an array. Note the "in". @@ -67,6 +67,26 @@ public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelP public extension ParentMoleculeModelProtocol { + /// Recursively finds and replaces the first child matching the replacement molecule id property. + mutating func deepReplaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + var possibleError: Error? + // Dive into each root. + depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in + guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } + do { + replacedMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) + } catch { + possibleError = error + } + stop = replacedMolecule != nil || possibleError != nil + } + if let error = possibleError { + throw error + } + return replacedMolecule + } + func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int) -> Result) -> Result { var result = initialResult if (options == .parentFirst) { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index b598041b..b3d8bb59 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -40,31 +40,3 @@ public extension TemplateModelProtocol { return rootMolecules.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit) } } - -extension TemplateModelProtocol { - - /// Recursively finds and replaces the first child matching the replacement molecule id property. - mutating func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { - // Attempt root level replacement on the template model first. - if let replacedMolecule = try replaceChildMolecule(with: replacementMolecule) { - return replacedMolecule - } - - var replacedMolecule: MoleculeModelProtocol? - var possibleError: Error? - // Dive into each root thereafter. - depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in - guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } - do { - replacedMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) - } catch { - possibleError = error - } - stop = replacedMolecule != nil || possibleError != nil - } - if let error = possibleError { - throw error - } - return replacedMolecule - } -} diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index c03594d4..9cbf85ed 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -41,7 +41,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co 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)") + Self.debugLog("Initializing for \(moleculeIds)") } public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [MoleculeModelProtocol]? { @@ -85,7 +85,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co moleculeModels.forEach { newMolecule in do { - if let replacedMolecule = try parentMolecule.replaceChildMolecule(with: newMolecule) { + if let replacedMolecule = try parentMolecule.deepReplaceMolecule(with: newMolecule) { guard !replacedMolecule.deepEquals(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("deep molecule \(newMolecule) is the same as \(replacedMolecule). skipping...") From 2ce7a7dbc653ee85003c440971846d17b8a2ce1c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Thu, 13 Jun 2024 10:19:13 -0400 Subject: [PATCH 3/3] Digital PCT265 story DE307-731: Earlier behavior application. Sumanth catch. --- MVMCoreUI/BaseControllers/ViewController.swift | 12 ++++++++---- .../Protocols/PageBehaviorHandlerProtocol.swift | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 15b172ce..78191da5 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -146,6 +146,11 @@ import MVMCore do { let template = try parsePageJSON(loadObject: loadObject) pageModel = template // TODO: Eventually this page parsing should be done outside of this class and then set by the caller. For now, double duty. + // Needed for PageMoleculeTransformationBehavior + PageLocalDataShareBehavior behaviors. + if let behaviorContainer = template as? (PageBehaviorContainerModelProtocol & TemplateModelProtocol) { + var behaviorHandler = self + behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer) + } isFirstRender = true // Assuming this is only on the first page load from the handler. Might need to revist later. if let backgroundRequest = loadObject.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject.identifier { MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil)) @@ -230,11 +235,11 @@ import MVMCore open func handleNewData(_ pageModel: PageModelProtocol? = nil, shouldTriggerRender: Bool = true) { guard var newPageModel = pageModel ?? self.pageModel else { return } - let originalModel = isFirstRender ? nil : self.pageModel as? MVMControllerModelProtocol + let originalModel = self.pageModel as? MVMControllerModelProtocol - // Refresh our behaviors if there is a page change. + // Refresh our behaviors if there is a page change. Originally set up in shouldFinishProcessingLoad. if let behaviorContainer = newPageModel as? (PageBehaviorContainerModelProtocol & TemplateModelProtocol), - (originalModel == nil || originalModel!.id != behaviorContainer.id) { + (originalModel == nil || originalModel!.id != behaviorContainer.id) { var behaviorHandler = self behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer) } @@ -277,7 +282,6 @@ import MVMCore let allUpdatedMolecules = behaviorUpdatedModels //+ pageUpdatedModels // 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) guard shouldTriggerRender else { return } diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift index 566a3f3d..ad50b9ed 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift @@ -41,7 +41,7 @@ public extension PageBehaviorHandlerProtocol { // Apply them to the page. self.behaviors = behaviors.count > 0 ? behaviors : nil - // Ask the session to apply any more. (Curently inverted contol due to Swift <--> Obj-C conflict. + // Ask the session to apply any more. (Currently inverted contol due to Swift <--> Obj-C conflict.) if let viewController = self as? UIViewController { MVMCoreUISession.sharedGlobal()?.applyGlobalBehaviors(to: viewController) }