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 6ed0c584..585944bf 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() @@ -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...") prepareMolecules(with: carouselModel) @@ -229,7 +231,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..24a0bb02 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 { @@ -16,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/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) } 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...")