From 525f0f8f0abbb84d8184dac91f616181edd827f6 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 09:13:49 -0400 Subject: [PATCH 1/8] Digital PCT265 defect CXTDT-579049: Add indexing safety to track action call. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 3ef117da..fc79b5bd 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -361,7 +361,7 @@ open class Carousel: View { } func trackSwipeActionAnalyticsforIndex(_ index : Int){ - guard let itemModel = molecules?[index], + guard let itemModel = molecules?[safe:index], let viewControllerObject = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return } MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: viewControllerObject, actionInformation: itemModel.toJSON(), additionalData: nil) } From 080f77581f6d48a4cc21a646cd481bc7e7c5ff64 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 09:18:13 -0400 Subject: [PATCH 2/8] Digital PCT265 defect CXTDT-579049: Fix indexing safety to mocules to add. (Another crash in App Store reports.) --- MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index 943f53ad..22ec9bcd 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -108,8 +108,7 @@ extension TabsListItemModel: AddMolecules { public func moleculesToAdd() -> AddMolecules.AddParameters? { guard addedMolecules == nil else { return nil } let index = tabs.selectedIndex - guard molecules.count >= index else { return nil } - let addedMolecules = molecules[index] + guard let addedMolecules = molecules[safe: index] else { return nil } self.addedMolecules = addedMolecules return (addedMolecules, .below) } From 91eb4fa87a793c141c22563a9540e541460f37e9 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 15:23:17 -0400 Subject: [PATCH 3/8] Digital PCT265 defect CXTDT-579050: Prevent updateViews from triggering tableView(_:cellForRowAt:) through visibleCells. --- .../ThreeLayerTableViewController.swift | 4 ++-- MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index 4bd889bd..c85e7b85 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -52,8 +52,8 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController, Rotor bottomView.updateView(width) showFooter(width) } - tableView.visibleCells.forEach { cell in - (cell as? MVMCoreViewProtocol)?.updateView(width) + MVMCoreUIUtility.findParentViews(by: (UITableViewCell & MVMCoreViewProtocol).self, views: tableView.subviews).forEach { view in + view.updateView(width) } } diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift index ea91f62b..5b70412e 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift +++ b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift @@ -60,6 +60,16 @@ public extension MVMCoreUIUtility { return findViews(by: type, views: queue, excludedViews: excludedViews) + matching } + static func findParentViews(by type: T.Type, views: [UIView]) -> [T] { + return views.reduce(into: [T]()) { matchingViews, view in + if let view = view as? T { + return matchingViews.append(view) // If this view is the type stop here and return, ignoring its children. + } + // Otherwise check downstream. + matchingViews += findParentViews(by: type, views: view.subviews) + } + } + @MainActor static func visibleNavigationBarStlye() -> NavigationItemStyle? { if let navController = NavigationController.navigationController(), From d8d4b37d1d423d2f4b7bf011d095183f2f788e02 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 2 Jul 2024 19:05:53 -0400 Subject: [PATCH 4/8] Digital PCT265 defect CXTDT-579050: Rewire asynchronous response JSON parsing to filter, queue, and delay drop UI changes. --- .../BaseControllers/ViewController.swift | 97 ++++++++++--------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 78191da5..e67188a0 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -8,6 +8,7 @@ import UIKit import MVMCore +import Combine @objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol { @@ -38,7 +39,7 @@ import MVMCore public var behaviors: [PageBehaviorProtocol]? public var needsUpdateUI = false - private var observingForResponses: NSObjectProtocol? + private var observingForResponses: AnyCancellable? private var initialLoadFinished = false public var isFirstRender = true public var previousScreenSize = CGSize.zero @@ -66,9 +67,46 @@ import MVMCore (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) else { return } - observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in - self?.responseJSONUpdated(notification: notification) - } + observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) + .receive(on: self.pageUpdateQueue) // Background serial queue. + .map { [weak self] notification in + guard let self = self else { return (nil, nil, nil) } + // Get the page data. + let pageUpdates = self.extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:]) + // Convert the page data into a new model. + var pageModel: PageModelProtocol? = nil + if let pageUpdates { + do { + // TODO: Rewiring to parse from plain JSON rather than this protocol indirection. + pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates) + } catch { + if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") { + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + } + } + } + // Get the module data. + let moduleUpdates = self.extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) + // Bundle the transformations. + return (pageUpdates, pageModel, moduleUpdates) + } + .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in + // Skip any non-updates. + (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) + } + // Opportunity: Merge all module only updates into one event. + // Delay allowing the previous model update to settle before triggering a re-render. + .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest) + .flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(0.1), scheduler: RunLoop.main) } + .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in + guard let self = self else { return } + if let pageUpdates, let pageModel { + self.loadObject?.pageJSON = pageUpdates + } + var mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) + self.loadObject?.modulesJSON = mergedModuleUpdates + self.handleNewData(pageModel) + } } open func stopObservingForResponseJSONUpdates() { @@ -88,51 +126,22 @@ import MVMCore return requestModules + behaviorModules } - @objc open func responseJSONUpdated(notification: Notification) { - // Checks for a page we are listening for. - 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), + private func extractInterestedPageType(from pageMap: [String: Any]) -> [String: Any]? { + guard let pageType = pagesToListenFor()?.first(where: { pageTypeListened -> Bool in + guard let page = pageMap.optionalDictionaryForKey(pageTypeListened), let pageType = page.optionalStringForKey(KeyPageType), pageType == pageTypeListened else { return false } - return true - }) { - hasDataUpdate = true - loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) - - // 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) - } - } + }) else { return nil } + return pageMap.optionalDictionaryForKey(pageType) + } + + private func extractInterestedModules(from moduleMap: [String: Any]) -> [String: Any]? { + guard let modulesListened = modulesToListenFor() else { return nil } + return moduleMap.filter { (key: String, value: Any) in + modulesListened.contains { $0 == key } } - - // Checks for modules we are listening for. - if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap), - let modulesListened = modulesToListenFor() { - for moduleName in modulesListened { - if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { - hasDataUpdate = true - var currentModules = loadObject?.modulesJSON ?? [:] - currentModules.updateValue(module, forKey: moduleName) - loadObject?.modulesJSON = currentModules - } - } - } - - guard hasDataUpdate else { return } - - MVMCoreDispatchUtility.performBlock(onMainThread: { - self.handleNewData(pageModel) - }) } open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer) -> Bool { From 4cbf15a3a75e250c033e7abcd7150da276f8fa86 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 15:42:12 -0400 Subject: [PATCH 5/8] Digital PCT265 defect CXTDT-579050: Change batching to the UI. --- .../BaseControllers/ViewController.swift | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index e67188a0..950b330e 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -70,41 +70,34 @@ import Combine observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .receive(on: self.pageUpdateQueue) // Background serial queue. .map { [weak self] notification in - guard let self = self else { return (nil, nil, nil) } - // Get the page data. - let pageUpdates = self.extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:]) - // Convert the page data into a new model. - var pageModel: PageModelProtocol? = nil - if let pageUpdates { - do { - // TODO: Rewiring to parse from plain JSON rather than this protocol indirection. - pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates) - } catch { - if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") { - MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) - } - } - } - // Get the module data. - let moduleUpdates = self.extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) - // Bundle the transformations. - return (pageUpdates, pageModel, moduleUpdates) + self?.pullUpdates(from: notification) ?? (nil, nil, nil) } .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in // Skip any non-updates. (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) } - // Opportunity: Merge all module only updates into one event. + // Merge all page and module updates into one update event. + //.print("[Update pipe] merging") + .scan((nil, nil, nil)) { accumulator, next in + // Always take the latest page and the latest modules with same key. + return (next.0, next.1, next.2?.mergingRight(accumulator.2 ?? [:])) + } + //.print("[Update pipe] into buffer") + // Hold onto the latest merged state until UI is ready for an update. Keep only the latest from scan. + .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) + //.print("[Update pipe] out of buffer") // Delay allowing the previous model update to settle before triggering a re-render. - .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest) - .flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(0.1), scheduler: RunLoop.main) } + .flatMap(maxPublishers: .max(1)) { buffer in + Just(buffer).delay(for: .seconds(0.1), scheduler: RunLoop.main) + } .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in guard let self = self else { return } - if let pageUpdates, let pageModel { + if let pageUpdates, pageModel != nil { self.loadObject?.pageJSON = pageUpdates } - var mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) + let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) self.loadObject?.modulesJSON = mergedModuleUpdates + self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page.") self.handleNewData(pageModel) } } @@ -115,6 +108,28 @@ import Combine self.observingForResponses = nil } + func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) { + // Get the page data. + let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:]) + // Convert the page data into a new model. + var pageModel: PageModelProtocol? = nil + if let pageUpdates { + do { + // TODO: Rewiring to parse from plain JSON rather than this protocol indirection. + pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates) + } catch { + if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") { + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + } + } + } + // Get the module data. + let moduleUpdates = extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) + debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")") + // Bundle the transformations. + return (pageUpdates, pageModel, moduleUpdates) + } + open func pagesToListenFor() -> [String]? { guard let pageType = loadObject?.pageType else { return nil } return [pageType] From 90f9a0bcf57cd7508d5971dbbbecaf761ee29d71 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:39:28 -0400 Subject: [PATCH 6/8] Digital PCT265 defect CXTDT-579050: Code simplification and fixes of combine pipeline for page updates. --- .../Atomic/Organisms/Carousel/Carousel.swift | 11 ++++++++ .../BaseControllers/ViewController.swift | 26 +++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index fc79b5bd..84124880 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -285,6 +285,17 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { + for molecule in carouselModel.molecules { + if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { + collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + } + } + let moleculeInfo = carouselModel.molecules.map { + getMoleculeInfo(with: $0, delegateObject: delegateObject) + } + // For each molecule with info, register it. + moleculeInfo.compactMap({ $0 }).forEach { collectionView.register($0.class, forCellWithReuseIdentifier: $0.identifier) } + for molecule in carouselModel.molecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 950b330e..a6c43b2d 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -69,27 +69,16 @@ import Combine observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .receive(on: self.pageUpdateQueue) // Background serial queue. - .map { [weak self] notification in - self?.pullUpdates(from: notification) ?? (nil, nil, nil) - } - .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in - // Skip any non-updates. - (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) + .compactMap { [weak self] notification in + self?.pullUpdates(from: notification) ?? nil } // Merge all page and module updates into one update event. - //.print("[Update pipe] merging") .scan((nil, nil, nil)) { accumulator, next in // Always take the latest page and the latest modules with same key. - return (next.0, next.1, next.2?.mergingRight(accumulator.2 ?? [:])) + return (next.0 ?? accumulator.0, next.1 ?? accumulator.1, next.2?.mergingRight(accumulator.2 ?? [:])) } - //.print("[Update pipe] into buffer") - // Hold onto the latest merged state until UI is ready for an update. Keep only the latest from scan. - .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) - //.print("[Update pipe] out of buffer") // Delay allowing the previous model update to settle before triggering a re-render. - .flatMap(maxPublishers: .max(1)) { buffer in - Just(buffer).delay(for: .seconds(0.1), scheduler: RunLoop.main) - } + .throttle(for: .seconds(0.05), scheduler: RunLoop.main, latest: true) .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in guard let self = self else { return } if let pageUpdates, pageModel != nil { @@ -108,7 +97,7 @@ import Combine self.observingForResponses = nil } - func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) { + func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)? { // Get the page data. let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:]) // Convert the page data into a new model. @@ -125,7 +114,10 @@ import Combine } // Get the module data. let moduleUpdates = extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) - debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")") + debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")") + + guard (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) else { return nil } + // Bundle the transformations. return (pageUpdates, pageModel, moduleUpdates) } From b2ad684f0081838c258af082fe0f7b0540f5e98c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:40:43 -0400 Subject: [PATCH 7/8] Digital PCT265 defect CXTDT-579050: Carousel cell registration check. --- .../Atomic/Organisms/Carousel/Carousel.swift | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 84124880..6df0f7c2 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -43,6 +43,9 @@ open class Carousel: View { /// The models for the molecules. public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]? + /// A list of currently registered cells. + public var registeredMoleculeIds: [String]? + /// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%. public var itemAlignment = UICollectionView.ScrollPosition.left @@ -174,9 +177,7 @@ 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), - 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. - { + if hasSameCellRegistration(with: carouselModel, delegateObject: delegateObject) { // 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) @@ -284,23 +285,27 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { - + var registeredIds = [String]() for molecule in carouselModel.molecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + registeredIds.append(info.identifier) + } else { + registeredIds.append(molecule.moleculeName) } } - let moleculeInfo = carouselModel.molecules.map { - getMoleculeInfo(with: $0, delegateObject: delegateObject) - } - // For each molecule with info, register it. - moleculeInfo.compactMap({ $0 }).forEach { collectionView.register($0.class, forCellWithReuseIdentifier: $0.identifier) } - - for molecule in carouselModel.molecules { + registeredMoleculeIds = registeredIds + } + + func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + let incomingIds = carouselModel.molecules.map { molecule in if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { - collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + return info.identifier + } else { + return molecule.moleculeName } } + return incomingIds == registeredMoleculeIds } //-------------------------------------------------- From cae92360a254a8a31975b4f7e3f9eb5f6959c7c7 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:45:29 -0400 Subject: [PATCH 8/8] Digital PCT265 defect CXTDT-579050: Carousel cell registration check break early. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 6df0f7c2..ad627d35 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -298,6 +298,8 @@ open class Carousel: View { } func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + guard let registeredMoleculeIds else { return false } + let incomingIds = carouselModel.molecules.map { molecule in if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { return info.identifier