From 47050e3c03c69c8acf55cb0e706a2420019f2b85 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 15 Nov 2023 18:02:50 -0500 Subject: [PATCH 01/64] remove gitlab stages to disable --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8dc2e960..19d0cb28 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ stages: # - test - - download - - build - - deploy +# - download +# - build +# - deploy #test: # stage: test From fca60ace66f7d6f867a7b41fe615c2ef6ebf8c79 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 15 Nov 2023 18:10:24 -0500 Subject: [PATCH 02/64] switch to manual runner --- .gitlab-ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19d0cb28..a23dac55 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ stages: # - test -# - download -# - build -# - deploy + - download + - build + - deploy #test: # stage: test @@ -12,6 +12,7 @@ stages: # - xcode_12_2 download_artifacts: + when: manual stage: download script: - ./Scripts/download_dependencies.sh @@ -27,6 +28,7 @@ download_artifacts: ARTIFACTORY_URL: https://oneartifactoryci.verizon.com/artifactory build_project: + when: manual stage: build script: - ./Scripts/build_aggregate.sh @@ -37,6 +39,7 @@ build_project: - xcode_12_2 deploy_snapshot: + when: manual stage: deploy script: - cd Scripts && ./upload_core_ui_frameworks.sh From c5dd5ed4e7d7b4ee619809d2b0239fb25dabe5a8 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Thu, 25 Apr 2024 19:34:36 -0400 Subject: [PATCH 03/64] Digital PCT265 story ONEAPP-7249 - Prevent forcing immediate layouts on cell changes which causes scroll stagger and carousel width double layouts. --- MVMCoreUI/Atomic/Templates/CollectionTemplate.swift | 2 +- MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift | 8 ++++---- MVMCoreUI/Atomic/Templates/SectionListTemplate.swift | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index b242c64a..100b1b17 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -134,7 +134,7 @@ } update(cell: cell, size: view.frame.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells - cell.layoutIfNeeded() + cell.setNeedsLayout() return cell } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index b825dc68..0c65d403 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -169,7 +169,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } (cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells - cell.layoutIfNeeded() + cell.setNeedsLayout() return cell } @@ -353,7 +353,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { indexPaths.count > 0 else { return } tableView?.deleteRows(at: indexPaths, with: animation) updateViewConstraints() - view.layoutIfNeeded() + view.setNeedsLayout() } public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) { @@ -372,7 +372,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { indexPaths.count > 0 else { return } self.tableView?.insertRows(at: indexPaths, with: animation) self.updateViewConstraints() - self.view.layoutIfNeeded() + self.view.setNeedsLayout() } public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) { @@ -420,7 +420,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { tableView.endUpdates() self.updateViewConstraints() - self.view.layoutIfNeeded() + self.view.setNeedsLayout() } public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { diff --git a/MVMCoreUI/Atomic/Templates/SectionListTemplate.swift b/MVMCoreUI/Atomic/Templates/SectionListTemplate.swift index 08a7d020..0feeadb8 100644 --- a/MVMCoreUI/Atomic/Templates/SectionListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/SectionListTemplate.swift @@ -112,7 +112,7 @@ open class SectionListTemplate: MoleculeListTemplate { (header as? MoleculeViewProtocol)?.set(with: headerInfo.molecule, delegateObjectIVar, nil) (header as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells - header?.layoutIfNeeded() + header?.setNeedsLayout() return header } @@ -126,7 +126,7 @@ open class SectionListTemplate: MoleculeListTemplate { (footer as? MoleculeViewProtocol)?.set(with: footerInfo.molecule, delegateObjectIVar, nil) (footer as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells - footer?.layoutIfNeeded() + footer?.setNeedsLayout() return footer } From 895f0181ecdfc2340c9f3787d30679afe78c2488 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Tue, 30 Apr 2024 16:03:48 -0400 Subject: [PATCH 04/64] Digital PCT265 story ONEAPP-7592 - Updates to tile container to allow custom colors. --- .../Atomic/Atoms/Views/TileContainerModel.swift | 3 ++- .../Atomic/Extensions/VDS-Enums+Codable.swift | 6 +++++- .../H1/HeadersH1NoButtonsBodyTextModel.swift | 6 +++--- MVMCoreUI/Categories/UIColor+Extension.swift | 6 ++++++ MVMCoreUI/Categories/UIColor+MFConvenience.m | 14 +++++++------- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift index 895bb027..2a6d98aa 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift @@ -8,6 +8,7 @@ import Foundation import VDS +import MVMCore open class TileContainerModel: TileContainerBaseModel, ParentMoleculeModelProtocol, MoleculeModelProtocol { @@ -36,7 +37,7 @@ open class TileContainerModel: TileContainerBaseModel TitleLockupModel { guard let headline = headlineBody.headline else { throw ModelRegistry.Error.decoderOther(message: "headline is required for this use case.") } - var body = headlineBody.body + let body = headlineBody.body switch headlineBody.style ?? defaultStyle { case .landingHeader: headline.fontStyle = Styler.Font.RegularTitle2XLarge @@ -103,13 +103,13 @@ public struct DeprecatedHeadlineBodyHelper { headline.fontStyle = Styler.Font.RegularTitleXLarge body?.fontStyle = Styler.Font.RegularTitleMedium } - let model = try TitleLockupModel(title: headline, subTitle: body) + let model = TitleLockupModel(title: headline, subTitle: body) model.id = headlineBody.id return model } public func createHeadlineBodyModel(titleLockup: TitleLockupModel) -> HeadlineBodyModel { - var headlineBody = HeadlineBodyModel(headline: titleLockup.title) + let headlineBody = HeadlineBodyModel(headline: titleLockup.title) headlineBody.body = titleLockup.subTitle headlineBody.id = titleLockup.id return headlineBody diff --git a/MVMCoreUI/Categories/UIColor+Extension.swift b/MVMCoreUI/Categories/UIColor+Extension.swift index f4ef0f7f..1d2158a6 100644 --- a/MVMCoreUI/Categories/UIColor+Extension.swift +++ b/MVMCoreUI/Categories/UIColor+Extension.swift @@ -230,6 +230,12 @@ extension UIColor { return UIColor(named: name, in: MVMCoreUIUtility.bundleForMVMCoreUI(), compatibleWith: nil)! } + /// Returns a color corresponding to the passed in color name. + @objc + public static func mvmCoreUIColor(with name: String) -> UIColor? { + return UIColor.names[name]?.uiColor + } + /// Convenience to get a grayscale UIColor where the same value is used for red, green, and blue. public class func grayscale(rgb: Int, alpha: CGFloat = 1.0) -> UIColor { diff --git a/MVMCoreUI/Categories/UIColor+MFConvenience.m b/MVMCoreUI/Categories/UIColor+MFConvenience.m index cd809509..f33b71b0 100644 --- a/MVMCoreUI/Categories/UIColor+MFConvenience.m +++ b/MVMCoreUI/Categories/UIColor+MFConvenience.m @@ -7,6 +7,7 @@ // #import "UIColor+MFConvenience.h" +#import @import MVMCore.MVMCoreDispatchUtility; @implementation UIColor (MFConvenience) @@ -298,6 +299,10 @@ } + (nullable UIColor *)mfGetColorForString:(nullable NSString *)string { + if ([string hasPrefix:@"#"]) { + return [self mfGetColorForHex:string]; + } + static NSDictionary *stringColorMapping; static dispatch_once_t once; dispatch_once(&once, ^{ @@ -327,14 +332,9 @@ UIColor *color = nil; if (string && string.length > 0) { - color = [stringColorMapping objectForKey:string]; - if (!color){ - color = [UIColor blackColor]; - } - } else { - color = [UIColor blackColor]; + color = [stringColorMapping objectForKey:string] ?: [UIColor mvmCoreUIColorWith:string]; } - return color; + return color ?: [UIColor blackColor]; } + (nonnull UIColor *)mfGetColorForHex:(nonnull NSString *) hexString { From a0168d6e38ccaab86dde7031fc3b464b985d9bb6 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Tue, 30 Apr 2024 16:13:25 -0400 Subject: [PATCH 05/64] Digital PCT265 story ONEAPP-7592 - Warning fixes --- .../Item Dropdown/BaseItemPickerEntryField.swift | 2 +- .../Atoms/FormFields/TextFields/TextEntryField.swift | 2 +- .../VerticalCombinationViews/HeadlineBodyModel.swift | 4 ++-- MVMCoreUI/Behaviors/GetContactBehavior.swift | 8 ++++---- MVMCoreUI/Categories/UIStackView+Extension.swift | 3 ++- MVMCoreUI/Managers/SubNav/SubNavManagerController.swift | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift index 65c4448b..22a18b94 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift @@ -74,7 +74,7 @@ extension BaseItemPickerEntryField { @objc open override func setAccessibilityString(_ accessibilityString: String?) { - var accessibilityString = accessibilityString ?? "" + let accessibilityString = accessibilityString ?? "" textField.accessibilityTraits = .staticText textField.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item") textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index a3780846..9d030d0f 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -397,7 +397,7 @@ extension TextEntryField { @objc open override func setAccessibilityString(_ accessibilityString: String?) { - var accessibilityString = accessibilityString ?? "" + let accessibilityString = accessibilityString ?? "" textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index 093528d1..c8c1a633 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -94,7 +94,7 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { public extension HeadlineBodyModel { func createHeaderTitleLockupModel(defaultStyle: Style = .header) throws -> TitleLockupModel { guard let headline = headline else { throw ModelRegistry.Error.decoderOther(message: "headline is required for this use case.") } - var body = self.body + let body = self.body switch style ?? defaultStyle { case .landingHeader: headline.fontStyle = Styler.Font.RegularTitle2XLarge @@ -106,7 +106,7 @@ public extension HeadlineBodyModel { headline.fontStyle = Styler.Font.RegularTitleXLarge body?.fontStyle = Styler.Font.RegularTitleMedium } - let model = try TitleLockupModel(title: headline, subTitle: body) + let model = TitleLockupModel(title: headline, subTitle: body) model.id = id return model } diff --git a/MVMCoreUI/Behaviors/GetContactBehavior.swift b/MVMCoreUI/Behaviors/GetContactBehavior.swift index 6f9a3cd4..755c5706 100644 --- a/MVMCoreUI/Behaviors/GetContactBehavior.swift +++ b/MVMCoreUI/Behaviors/GetContactBehavior.swift @@ -35,16 +35,16 @@ public class PageGetContactBehavior: PageVisibilityBehavior { CNContactStore().requestAccess(for: .contacts) { [weak self] (access, error) in guard access, error == nil, - let rootMolecules = self?.delegate?.moleculeDelegate?.getRootMolecules() else { return } + let rootMolecules = delegateObject?.moleculeDelegate?.getRootMolecules() else { return } // Iterate models and provide contact self?.getContacts(for: rootMolecules) // Tell template to update - MVMCoreDispatchUtility.performBlock(onMainThread: { + Task { @MainActor in // TODO: move to protocol function instead - guard let controller = self?.delegate?.moleculeDelegate as? ViewController else { return } + guard let controller = delegateObject?.moleculeDelegate as? ViewController else { return } controller.handleNewData() - }) + } } } diff --git a/MVMCoreUI/Categories/UIStackView+Extension.swift b/MVMCoreUI/Categories/UIStackView+Extension.swift index b6bc1b05..e6ed0dda 100644 --- a/MVMCoreUI/Categories/UIStackView+Extension.swift +++ b/MVMCoreUI/Categories/UIStackView+Extension.swift @@ -7,6 +7,7 @@ // import Foundation +import MVMCore extension UIStackView: MVMCoreViewProtocol { public func updateView(_ size: CGFloat) { @@ -16,7 +17,7 @@ extension UIStackView: MVMCoreViewProtocol { } /// A convenience function for updating molecules. If model is nil, view is hidden. - open func updateContainedMolecules(with models: [MoleculeModelProtocol?], _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + public func updateContainedMolecules(with models: [MoleculeModelProtocol?], _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { for (index, item) in arrangedSubviews.enumerated() { if let model = models[index] { (item as? MoleculeViewProtocol)?.set(with: model, delegateObject, additionalData) diff --git a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift index 2c2e4ae7..2bde433a 100644 --- a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift +++ b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift @@ -404,7 +404,7 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, public func update(percentage: CGFloat) { guard customInteractor?.interactive == true, - let index = index else { return } + let _ = index else { return } // tabs.progress(from: tabs.selectedIndex, toIndex: index, percentage: percentage) } } From 3f77a261bc8dd719e65debba02723d4da55b1af7 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 30 Apr 2024 20:36:49 -0400 Subject: [PATCH 06/64] Digital PCT265 story ONEAPP-7249 - Polling behavior fixes and logging for clarity. --- .../MoleculeComparisonProtocol.swift | 9 ++++ .../Behaviors/PollingBehaviorModel.swift | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift new file mode 100644 index 00000000..f2ac0715 --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -0,0 +1,9 @@ +// +// MoleculeComparisonProtocol.swift +// MVMCoreUI +// +// Created by Kyle Hedden on 4/29/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index 7a92941f..5e1058d8 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -8,6 +8,7 @@ import Foundation import MVMCore +import Combine public class PollingBehaviorModel: PageBehaviorModelProtocol { public class var identifier: String { "pollingBehavior" } @@ -47,15 +48,24 @@ public class PollingBehaviorModel: PageBehaviorModelProtocol { } } -public class PollingBehavior: NSObject, PageVisibilityBehavior { +extension PollingBehaviorModel: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(Self.self) @ \(refreshInterval) firing \(self.refreshAction)" + } +} + +public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTransformationBehavior, CoreLogging { + + public static var loggingCategory: String? { return String(describing: Self.self) } var model: PollingBehaviorModel var delegateObject: MVMCoreUIDelegateObject? - var lastRefresh = Date.distantPast + var lastRefresh = Date() // Treat the last refresh as now on init. refreshOnShown will bypass otherwise. var pollTimer: DispatchSourceTimer? + var backgroundEventSubscripiton: AnyCancellable? var remainingTimeToRefresh: TimeInterval { - lastRefresh.timeIntervalSinceNow - model.refreshInterval + model.refreshInterval + lastRefresh.timeIntervalSinceNow // timeIntervalSinceNow in negative since earlier recording (--) } var firstTimeLoad = true @@ -71,6 +81,14 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior { public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { self.model = model as! PollingBehaviorModel self.delegateObject = delegateObject + Self.debugLog("Initializing for \(model)") + } + + public func onPageNew(rootMolecules: [any MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + 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. + resumePollingTimer(withRemainingTime: refreshOnShown ? 0 : remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval) + } } public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { @@ -79,23 +97,43 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior { public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) { pollTimer?.cancel() + backgroundEventSubscripiton = nil } func resumePollingTimer(withRemainingTime timeRemaining: TimeInterval, refreshAction: ActionModelProtocol, interval: TimeInterval) { let delegateObject = delegateObject pollTimer?.cancel() + let pollingId = UUID().uuidString + debugLog("Scheduling timed event \(pollingId) in \(timeRemaining), interval: \(interval)") pollTimer = DispatchSource.makeTimerSource() pollTimer?.schedule(deadline: .now() + timeRemaining, repeating: interval) - pollTimer?.setEventHandler(qos:.utility) { - Task { + pollTimer?.setEventHandler(qos:.utility) { [weak self] in + guard let self = self else { return } + lastRefresh = Date() + Task { [weak self] in + self?.debugLog("Firing timed event \(pollingId), \(refreshAction)") if let delegateActionHandler = delegateObject?.actionDelegate as? ActionDelegateProtocol { try? await delegateActionHandler.performAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) } else { try? await MVMCoreActionHandler.shared()?.handleAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) } + self?.debugLog("Finished timed event \(pollingId)") } } pollTimer?.resume() + setupBackgroundingPause() + } + + private func setupBackgroundingPause() { + backgroundEventSubscripiton = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification).sink { [weak self] _ in + guard let self = self else { return } + pollTimer?.cancel() + + backgroundEventSubscripiton = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).sink { [weak self] _ in + guard let self = self else { return } + resumePollingTimer(withRemainingTime: remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval) + } + } } deinit { From fd296b9623727d06af8333ecc1df43b573f68739 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Thu, 2 May 2024 19:21:29 -0400 Subject: [PATCH 07/64] Digital PCT265 story ONEAPP-7249 - Reduce full cell reloads on updates. --- .../Extensions/ReadableDecodingErrors.swift | 63 ------------------- .../Templates/MoleculeListTemplate.swift | 36 ++++++++--- .../ReplaceableMoleculeBehaviorModel.swift | 1 + 3 files changed, 29 insertions(+), 71 deletions(-) delete mode 100644 MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift diff --git a/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift b/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift deleted file mode 100644 index 4b48b0a4..00000000 --- a/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// ReadableDecodingErrors.swift -// MVMCore -// -// Created by Kyle Hedden on 10/5/23. -// Copyright © 2023 myverizon. All rights reserved. -// - -import Foundation - -protocol HumanReadableDecodingErrorProtocol { - var readableDescription: String { get } -} - -extension JSONError: HumanReadableDecodingErrorProtocol { - var readableDescription: String { - switch (self) { - case .other(let other): - if let other = other as? HumanReadableDecodingErrorProtocol { - return other.readableDescription - } - return description - default: - return description - } - } -} - -extension ModelRegistry.Error: HumanReadableDecodingErrorProtocol { - var readableDescription: String { - switch (self) { - case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil: - return "Model identifier \"\(identifier!)\" is not mapped for \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })" - - case .decoderErrorObjectNotPresent(let codingKey, let codingPath): - return "Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })" - - default: - return "Registry error: \((self as NSError).localizedFailureReason ?? self.localizedDescription)" - } - } -} - -extension DecodingError: HumanReadableDecodingErrorProtocol { - var readableDescription: String { - switch (self) { - case .keyNotFound(let codingKey, let context): - return "Required key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })" - - case .valueNotFound(_, let context): - return "Value not found @ \(context.codingPath.map { return $0.stringValue })" - - case .typeMismatch(_, let context): - return "Value type mismatch @ \(context.codingPath.map { return $0.stringValue })" - - case .dataCorrupted(let context): - return "Data corrupted @ \(context.codingPath.map { return $0.stringValue })" - - @unknown default: - return (self as NSError).localizedFailureReason ?? self.localizedDescription - } - } -} diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 0c65d403..d62fd149 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -97,12 +97,16 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false super.updateUI(for: molecules) - molecules?.forEach({ molecule in + guard let molecules else { return } + + // For updating individual specfied molecules. (Not a full table reload done in the base class.) These molecule types should remain the same type by replacement standards. + molecules.forEach({ molecule in + // Replace any top level cell data if required. if let index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) { moleculesInfo?[index].molecule = molecule } - newData(for: molecule) }) + newData(for: molecules) } open override func viewDidAppear(_ animated: Bool) { @@ -233,18 +237,27 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } open func newData(for molecule: MoleculeModelProtocol) { + newData(for: [molecule]) + } + + /// Refreshes all relevant cells for a given set of molecule models. + open func newData(for molecules: [MoleculeModelProtocol]) { //Check header and footer if replace happens then return. - if updateHeaderFooterView(topView, with: molecule) || - updateHeaderFooterView(bottomView, with: molecule, isHeader: false) { - return + molecules.forEach { + if updateHeaderFooterView(topView, with: $0) || + updateHeaderFooterView(bottomView, with: $0, isHeader: false) { + return + } } guard let moleculesInfo = moleculesInfo else { return } let indicies = moleculesInfo.indices.filter({ index -> Bool in - return moleculesInfo[index].molecule.findFirstMolecule(by: { - $0.moleculeName == molecule.moleculeName && equal(moleculeA: molecule, moleculeB: $0) + return moleculesInfo[index].molecule.findFirstMolecule(by: { existingMolecule in + molecules.contains { newMolecule in + existingMolecule.moleculeName == newMolecule.moleculeName && equal(moleculeA: existingMolecule, moleculeB: newMolecule) + } }) != nil }) @@ -253,7 +266,14 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol let indexPaths = indicies.map { return IndexPath(row: $0, section: 0) } - tableView.reloadRows(at: indexPaths, with: .automatic) + + if #available(iOS 15.0, *) { + // All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to levearage this more efficient method. + tableView.reconfigureRows(at: indexPaths) + } else { + // A full reload can cause a flicker / animation. Better to avoid with above reconfigure method. + tableView.reloadRows(at: indexPaths, with: .automatic) + } if let selectedIndex = selectedIndex { tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none) } diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 51428234..233efdcc 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -60,6 +60,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { } } 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) } } From 6f92282a1d7ac023db1b7bb28e434863cfb78d00 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 3 May 2024 12:54:17 -0400 Subject: [PATCH 08/64] Digital ACT191 defect - Carousel indicator disappearing fix. --- .../CarouselIndicatorModel.swift | 2 +- .../Atomic/Organisms/Carousel/Carousel.swift | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 3631bfc0..11bcdd8d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -26,7 +26,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro /// Sets the current Index to focus on. public var currentIndex: Int = 0 public var animated: Bool = true - public var hidesForSinglePage: Bool = false + public var hidesForSinglePage: Bool = true public var inverted: Bool = false /// Set true to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is false public var accessibilityHasSlidesInsteadOfPage: Bool = false diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index c4f7d825..5be4f6bb 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -69,7 +69,7 @@ open class Carousel: View { public var delegateObject: MVMCoreUIDelegateObject? private var size: CGFloat? - + // Updates the model and index. public func updateModelIndex() { (model as? CarouselModel)?.index = pageIndex @@ -90,12 +90,29 @@ 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, - (model.paging == true || loop == true) else { return } + guard let model = model as? CarouselModel, !model.molecules.isEmpty else { return } + guard (model.paging == true || loop == true) else { + DispatchQueue.main.async { [self] in + updatePagerVisibility() + } + return + } DispatchQueue.main.async { [self] in collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: false) collectionView.layoutIfNeeded() showPeaking(true) + updatePagerVisibility() + } + } + + /// Updates if the pager is visible or not. + private func updatePagerVisibility() { + guard let pagingView = pagingView else { return } + let shouldHidePager = collectionView.contentSize.width < bounds.width + if (shouldHidePager && !pagingView.isHidden) || (!shouldHidePager && pagingView.isHidden) { + pagingView.isHidden = shouldHidePager + pagingBottomPin?.isActive = !shouldHidePager + delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self) } } @@ -140,16 +157,6 @@ open class Carousel: View { (cell as? MVMCoreViewProtocol)?.updateView(size) } layoutCollection() - - // Check must be dispatched to main for the layout to complete in layoutCollection. - DispatchQueue.main.async { [self] in - let shouldHidePager = molecules?.count ?? 0 < 2 || collectionView.contentSize.width < bounds.width - if let pagingView = pagingView, shouldHidePager != pagingView.isHidden { - pagingView.isHidden = shouldHidePager - pagingBottomPin?.isActive = !shouldHidePager - delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self) - } - } } //-------------------------------------------------- @@ -244,7 +251,7 @@ open class Carousel: View { var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)? = nil if let molecule = molecule, - (!molecule.hidesForSinglePage || numberOfPages > 1) { + (numberOfPages > 1 || molecule.hidesForSinglePage) { pagingView = ModelRegistry.createMolecule(molecule, delegateObject: delegateObject) as? (MoleculeViewProtocol & CarouselPageControlProtocol) pagingMoleculeName = molecule.moleculeName } else { From 1c953bcccbe7cc66a42fe07c6adc7855295d6d00 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 3 May 2024 15:02:06 -0400 Subject: [PATCH 09/64] Digital ACT191 defect - fix logic flip --- 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 5be4f6bb..18d65559 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -251,7 +251,7 @@ open class Carousel: View { var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)? = nil if let molecule = molecule, - (numberOfPages > 1 || molecule.hidesForSinglePage) { + (numberOfPages > 1 || !molecule.hidesForSinglePage) { pagingView = ModelRegistry.createMolecule(molecule, delegateObject: delegateObject) as? (MoleculeViewProtocol & CarouselPageControlProtocol) pagingMoleculeName = molecule.moleculeName } else { From d7fd6f4465d0e1427d2318e8c192b39122d564da Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 13:26:39 -0500 Subject: [PATCH 10/64] added eyebrowModel Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift | 1 + .../Atomic/Atoms/Views/TileletModel.swift | 51 +++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift index 9b3eb87b..58a91de5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift @@ -45,6 +45,7 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{ } else if let percentage = viewModel.textPercentage { textWidth = .percentage(percentage) } + eyebrowModel = viewModel.eyebrowModel(delegateObject: delegateObject, additionalData: additionalData) titleModel = viewModel.titleModel(delegateObject: delegateObject, additionalData: additionalData) subTitleModel = viewModel.subTitleModel(delegateObject: delegateObject, additionalData: additionalData) badgeModel = viewModel.badge diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index b287a9ff..b6fdd642 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -19,6 +19,7 @@ open class TileletModel: TileContainerBaseModel, Molec public var backgroundColor: Color? public var badge: Tilelet.BadgeModel? + public var eyebrow: LabelModel? public var title: LabelModel? public var subTitle: LabelModel? public var descriptiveIcon: Tilelet.DescriptiveIcon? @@ -30,6 +31,7 @@ open class TileletModel: TileContainerBaseModel, Molec case id case moleculeName case badge + case eyebrow case title case subTitle case descriptiveIcon @@ -41,6 +43,7 @@ open class TileletModel: TileContainerBaseModel, Molec let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString badge = try container.decodeIfPresent(Tilelet.BadgeModel.self, forKey: .badge) + eyebrow = try container.decodeIfPresent(LabelModel.self, forKey: .eyebrow) title = try container.decodeIfPresent(LabelModel.self, forKey: .title) subTitle = try container.decodeIfPresent(LabelModel.self, forKey: .subTitle) descriptiveIcon = try container.decodeIfPresent(Tilelet.DescriptiveIcon.self, forKey: .descriptiveIcon) @@ -50,13 +53,43 @@ open class TileletModel: TileContainerBaseModel, Molec try super.init(from: decoder) } + public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.EyebrowModel? { + guard let eyebrow else { return nil } + + var eyebrowColor: TitleLockup.TextColor = .primary + if let color = eyebrow.textColor?.uiColor { + eyebrowColor = .custom(color, color) + } + + let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) + do { + if let style = eyebrow.fontStyle { + return .init(text: eyebrow.text, + textColor: eyebrowColor, + textAttributes: attrs, isBold: style.isBold(), + standardStyle: try style.vdsSubsetStyle()) + } + } catch MVMCoreError.errorObject(let object) { + MVMCoreLoggingHandler.shared()?.addError(toLog: object) + } catch { } + + return .init(text: eyebrow.text, textColor: eyebrowColor, textAttributes: attrs) + } + public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? { guard let title else { return nil } + + var titleColor: TitleLockup.TitleTextColor = .primary + if let color = title.textColor?.uiColor { + titleColor = .custom(color, color) + } + let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = title.fontStyle { return .init(text: title.text, + textColor: titleColor, textAttributes: attrs, standardStyle: try style.vdsSubsetStyle()) } @@ -65,23 +98,30 @@ open class TileletModel: TileContainerBaseModel, Molec MVMCoreLoggingHandler.shared()?.addError(toLog: object) } catch { } - return .init(text: title.text, textAttributes: attrs) + return .init(text: title.text, textColor: titleColor, textAttributes: attrs) } public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.SubTitleModel? { guard let subTitle else { return nil } + + var subTitleColor: TitleLockup.TextColor = .primary + if let color = subTitle.textColor?.uiColor { + subTitleColor = .custom(color, color) + } + let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = subTitle.fontStyle { return .init(text: subTitle.text, - otherStandardStyle: try style.vdsSubsetStyle(), + otherStandardStyle: try style.vdsSubsetStyle(), + textColor: subTitleColor, textAttributes: attrs) } } catch MVMCoreError.errorObject(let object) { MVMCoreLoggingHandler.shared()?.addError(toLog: object) } catch { } - return .init(text: subTitle.text, textAttributes: attrs) + return .init(text: subTitle.text, textColor: subTitleColor, textAttributes: attrs) } public override func encode(to encoder: Encoder) throws { @@ -89,8 +129,9 @@ open class TileletModel: TileContainerBaseModel, Molec try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(badge, forKey: .badge) - try container.encodeIfPresent(title, forKey: .title) - try container.encodeIfPresent(subTitle, forKey: .subTitle) + try container.encodeModelIfPresent(eyebrow, forKey: .eyebrow) + try container.encodeModelIfPresent(title, forKey: .title) + try container.encodeModelIfPresent(subTitle, forKey: .subTitle) try container.encodeIfPresent(descriptiveIcon, forKey: .descriptiveIcon) try container.encodeIfPresent(directionalIcon, forKey: .directionalIcon) try container.encodeIfPresent(textWidth, forKey: .textWidth) From 3e8c53ecdf7d5d531dc4b2af24c410fa37dff6dc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 13:28:17 -0500 Subject: [PATCH 11/64] updated enums Signed-off-by: Matt Bruce --- .../Atomic/Extensions/VDS-Enums+Codable.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index e8b5d90f..bdc20189 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -75,7 +75,7 @@ extension VDS.TileContainerBase.BackgroundColor: Codable { var container = encoder.singleValueContainer() switch self { case .custom(let value): - try container.encode(value) + try container.encode(Color(uiColor: value)) default: try container.encode(String(reflecting: self)) } @@ -96,9 +96,9 @@ extension VDS.TileContainerBase.BackgroundColor: Codable { self = .black default: if let color = try? Color(from: decoder) { - self = .custom(color.hex) + self = .custom(color.uiColor) } else { - self = .custom(type) + self = .custom(UIColor(hexString: type)) } } } @@ -128,9 +128,9 @@ extension VDS.TileContainerBase.BackgroundEffect: Codable { case .none: self = .none case .gradient: - let firstColor = try container.decode(String.self, forKey: .firstColor) - let secondColor = try container.decode(String.self, forKey: .secondColor) - self = .gradient(firstColor, secondColor) + let firstColor = try container.decode(Color.self, forKey: .firstColor) + let secondColor = try container.decode(Color.self, forKey: .secondColor) + self = .gradient(firstColor.uiColor, secondColor.uiColor) } } @@ -143,8 +143,8 @@ extension VDS.TileContainerBase.BackgroundEffect: Codable { try container.encode(BackgroundEffectType.none.rawValue, forKey: .type) case .gradient(let firstColor, let secondColor): try container.encode(BackgroundEffectType.gradient.rawValue, forKey: .type) - try container.encode(firstColor, forKey: .firstColor) - try container.encode(secondColor, forKey: .secondColor) + try container.encode(Color(uiColor: firstColor), forKey: .firstColor) + try container.encode(Color(uiColor: secondColor), forKey: .secondColor) @unknown default: break } From ddc1d5002c941987777cc90d6df1c2cd01ad40dd Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 13:29:26 -0500 Subject: [PATCH 12/64] removed subtitleColor since this is now textColor for the label added textColor into the API for the model to read textColor of label Signed-off-by: Matt Bruce --- .../LockUps/TitleLockupModel.swift | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index a280e0b4..dafe8c95 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -23,7 +23,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco public var eyebrow: LabelModel? public var title: LabelModel public var subTitle: LabelModel? - public var subTitleColor: Use = .primary public var alignment: VDS.TitleLockup.TextAlignment = .left public var inverted: Bool = false @@ -61,7 +60,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco case eyebrow case title case subTitle - case subTitleColor case inverted case alignment } @@ -78,17 +76,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow) subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle) - /// look for color hex code - if let color = try? typeContainer.decodeIfPresent(Color.self, forKey: .subTitleColor) { - self.subTitleColor = color.uiColor.isDark() ? .primary : .secondary - - } else if let subTitleColor = try? typeContainer.decodeIfPresent(Use.self, forKey: .subTitleColor) { - self.subTitleColor = subTitleColor - - } else { - subTitleColor = .primary - } - if let newAlignment = try typeContainer.decodeIfPresent(VDS.TitleLockup.TextAlignment.self, forKey: .alignment) { alignment = newAlignment } @@ -109,17 +96,23 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco try container.encodeIfPresent(eyebrow, forKey: .eyebrow) try container.encodeModel(title, forKey: .title) try container.encodeIfPresent(subTitle, forKey: .subTitle) - try container.encode(subTitleColor, forKey: .subTitleColor) try container.encode(alignment, forKey: .alignment) try container.encode(inverted, forKey: .inverted) } public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.EyebrowModel? { guard let eyebrow else { return nil } + + var eyebrowColor: TitleLockup.TextColor = .primary + if let color = eyebrow.textColor?.uiColor { + eyebrowColor = .custom(color, color) + } + let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = eyebrow.fontStyle { return .init(text: eyebrow.text, + textColor: eyebrowColor, isBold: style.isBold(), standardStyle: try style.vdsSubsetStyle(), textAttributes: attrs, @@ -129,14 +122,21 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco MVMCoreLoggingHandler.shared()?.addError(toLog: object) } catch { } - return .init(text: eyebrow.text, textAttributes: attrs, numberOfLines: eyebrow.numberOfLines ?? 0) + return .init(text: eyebrow.text, textColor: eyebrowColor, textAttributes: attrs, numberOfLines: eyebrow.numberOfLines ?? 0) } public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.TitleModel { + + var titleColor: TitleLockup.TitleTextColor = .primary + if let color = title.textColor?.uiColor { + titleColor = .custom(color, color) + } + let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = title.fontStyle { return .init(text: title.text, + textColor: titleColor, textAttributes: attrs, isBold: style.isBold(), standardStyle: try style.vdsSubsetStyle(), @@ -147,11 +147,17 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco MVMCoreLoggingHandler.shared()?.addError(toLog: object) } catch { } - return .init(text: title.text, textAttributes: attrs, numberOfLines: title.numberOfLines ?? 0) + return .init(text: title.text, textColor: titleColor, textAttributes: attrs, numberOfLines: title.numberOfLines ?? 0) } public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.SubTitleModel? { guard let subTitle else { return nil } + + var subTitleColor: TitleLockup.TextColor = .primary + if let color = subTitle.textColor?.uiColor { + subTitleColor = .custom(color, color) + } + let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { From c0d0e6f6272ae1d0fcc2bb541f05228b55efb458 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 13:29:47 -0500 Subject: [PATCH 13/64] added to look at the Color(name: after VDS Signed-off-by: Matt Bruce --- MVMCoreUI/CustomPrimitives/Color.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/CustomPrimitives/Color.swift b/MVMCoreUI/CustomPrimitives/Color.swift index 9245153d..678b845c 100644 --- a/MVMCoreUI/CustomPrimitives/Color.swift +++ b/MVMCoreUI/CustomPrimitives/Color.swift @@ -86,11 +86,14 @@ public final class Color: Codable { let colorString = try container.decode(String.self) if let vdsColor = UIColor.VDSColor(rawValue: colorString) { - self.uiColor = vdsColor.uiColor + uiColor = vdsColor.uiColor hex = uiColor.hexString ?? "" + } else if let color = Color(name: colorString) { + uiColor = color.uiColor + hex = color.hex } else { let components = try Color.getColorComponents(for: colorString) - self.uiColor = components.color + uiColor = components.color hex = components.hex name = components.name ?? "" } From d07825c7538e787360bacaf4915a7c0b8432c09e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 16:32:48 -0500 Subject: [PATCH 14/64] added back the TextColor enums for TitleLockup/Tilelet Signed-off-by: Matt Bruce --- .../Atomic/Atoms/Views/TileletModel.swift | 61 +++++++++---- .../Atomic/Extensions/VDS-Enums+Codable.swift | 89 +++++++++++++++++++ .../LockUps/TitleLockupModel.swift | 58 ++++++++---- 3 files changed, 171 insertions(+), 37 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index b6fdd642..ca90d6ae 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -20,8 +20,11 @@ open class TileletModel: TileContainerBaseModel, Molec public var badge: Tilelet.BadgeModel? public var eyebrow: LabelModel? + public var eyebrowColor: TitleLockup.TextColor = .primary public var title: LabelModel? + public var titleColor: TitleLockup.TitleTextColor = .primary public var subTitle: LabelModel? + public var subTitleColor: TitleLockup.TextColor = .primary public var descriptiveIcon: Tilelet.DescriptiveIcon? public var directionalIcon: Tilelet.DirectionalIcon? public var textWidth: CGFloat? @@ -32,8 +35,11 @@ open class TileletModel: TileContainerBaseModel, Molec case moleculeName case badge case eyebrow + case eyebrowColor case title + case titleColor case subTitle + case subTitleColor case descriptiveIcon case directionalIcon case textWidth @@ -50,17 +56,43 @@ open class TileletModel: TileContainerBaseModel, Molec directionalIcon = try container.decodeIfPresent(Tilelet.DirectionalIcon.self, forKey: .directionalIcon) textWidth = try container.decodeIfPresent(CGFloat.self, forKey: .textWidth) textPercentage = try container.decodeIfPresent(CGFloat.self, forKey: .textPercentage) + + /// look for color hex code + if let color = eyebrow?.textColor?.uiColor { + self.eyebrowColor = .custom(color, color) + + } else if let eyebrowColor = try? container.decodeIfPresent(TitleLockup.TextColor.self, forKey: .eyebrowColor) { + self.eyebrowColor = eyebrowColor + + } else { + eyebrowColor = .primary + } + + if let color = title?.textColor?.uiColor { + self.titleColor = .custom(color, color) + + } else if let titleColor = try? container.decodeIfPresent(TitleLockup.TitleTextColor.self, forKey: .titleColor) { + self.titleColor = titleColor + + } else { + titleColor = .primary + } + + if let color = subTitle?.textColor?.uiColor { + self.subTitleColor = .custom(color, color) + + } else if let subTitleColor = try? container.decodeIfPresent(TitleLockup.TextColor.self, forKey: .subTitleColor) { + self.subTitleColor = subTitleColor + + } else { + subTitleColor = .primary + } + try super.init(from: decoder) } public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.EyebrowModel? { guard let eyebrow else { return nil } - - var eyebrowColor: TitleLockup.TextColor = .primary - if let color = eyebrow.textColor?.uiColor { - eyebrowColor = .custom(color, color) - } - let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = eyebrow.fontStyle { @@ -78,12 +110,6 @@ open class TileletModel: TileContainerBaseModel, Molec public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? { guard let title else { return nil } - - var titleColor: TitleLockup.TitleTextColor = .primary - if let color = title.textColor?.uiColor { - titleColor = .custom(color, color) - } - let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { @@ -103,17 +129,11 @@ open class TileletModel: TileContainerBaseModel, Molec public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.SubTitleModel? { guard let subTitle else { return nil } - - var subTitleColor: TitleLockup.TextColor = .primary - if let color = subTitle.textColor?.uiColor { - subTitleColor = .custom(color, color) - } - let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = subTitle.fontStyle { return .init(text: subTitle.text, - otherStandardStyle: try style.vdsSubsetStyle(), + otherStandardStyle: try style.vdsSubsetStyle(), textColor: subTitleColor, textAttributes: attrs) } @@ -130,8 +150,11 @@ open class TileletModel: TileContainerBaseModel, Molec try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(badge, forKey: .badge) try container.encodeModelIfPresent(eyebrow, forKey: .eyebrow) + try container.encode(eyebrowColor, forKey: .eyebrowColor) try container.encodeModelIfPresent(title, forKey: .title) + try container.encode(titleColor, forKey: .titleColor) try container.encodeModelIfPresent(subTitle, forKey: .subTitle) + try container.encode(subTitleColor, forKey: .subTitleColor) try container.encodeIfPresent(descriptiveIcon, forKey: .descriptiveIcon) try container.encodeIfPresent(directionalIcon, forKey: .directionalIcon) try container.encodeIfPresent(textWidth, forKey: .textWidth) diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index bdc20189..8bb75a80 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -192,3 +192,92 @@ extension VDS.TileContainer.Padding: Codable { } } } + +extension VDS.TitleLockup.TextColor: Codable { + + enum CodingKeys: String, CodingKey { + case type + case lightColor + case darkColor + } + + enum CustomColorType: String, Codable { + case primary + case secondary + case custom + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(CustomColorType.self, forKey: .type) + + switch type { + case .primary: + self = .primary + case .secondary: + self = .secondary + case .custom: + let lightColor = try container.decode(Color.self, forKey: .lightColor) + let darkColor = try container.decode(Color.self, forKey: .darkColor) + self = .custom(lightColor.uiColor, darkColor.uiColor) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .primary: + try container.encode(CustomColorType.primary.rawValue, forKey: .type) + case .secondary: + try container.encode(CustomColorType.secondary.rawValue, forKey: .type) + case .custom(let lightColor, let darkColor): + try container.encode(CustomColorType.custom.rawValue, forKey: .type) + try container.encode(Color(uiColor: lightColor), forKey: .lightColor) + try container.encode(Color(uiColor: darkColor), forKey: .darkColor) + @unknown default: + break + } + } +} + +extension VDS.TitleLockup.TitleTextColor: Codable { + + enum CodingKeys: String, CodingKey { + case type + case lightColor + case darkColor + } + + enum CustomColorType: String, Codable { + case primary + case custom + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(CustomColorType.self, forKey: .type) + + switch type { + case .primary: + self = .primary + case .custom: + let lightColor = try container.decode(Color.self, forKey: .lightColor) + let darkColor = try container.decode(Color.self, forKey: .darkColor) + self = .custom(lightColor.uiColor, darkColor.uiColor) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .primary: + try container.encode(CustomColorType.primary.rawValue, forKey: .type) + case .custom(let lightColor, let darkColor): + try container.encode(CustomColorType.custom.rawValue, forKey: .type) + try container.encode(Color(uiColor: lightColor), forKey: .lightColor) + try container.encode(Color(uiColor: darkColor), forKey: .darkColor) + @unknown default: + break + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index dafe8c95..8296639a 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -21,8 +21,11 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco public var textAlignment: TitleLockup.TextAlignment = .left public var eyebrow: LabelModel? + public var eyebrowColor: TitleLockup.TextColor = .primary public var title: LabelModel + public var titleColor: TitleLockup.TitleTextColor = .primary public var subTitle: LabelModel? + public var subTitleColor: TitleLockup.TextColor = .primary public var alignment: VDS.TitleLockup.TextAlignment = .left public var inverted: Bool = false @@ -58,8 +61,11 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco case moleculeName case textAlignment case eyebrow + case eyebrowColor case title + case titleColor case subTitle + case subTitleColor case inverted case alignment } @@ -76,6 +82,37 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow) subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle) + /// look for color hex code + if let color = eyebrow?.textColor?.uiColor { + self.eyebrowColor = .custom(color, color) + + } else if let eyebrowColor = try? typeContainer.decodeIfPresent(TitleLockup.TextColor.self, forKey: .eyebrowColor) { + self.eyebrowColor = eyebrowColor + + } else { + eyebrowColor = .primary + } + + if let color = title.textColor?.uiColor { + self.titleColor = .custom(color, color) + + } else if let titleColor = try? typeContainer.decodeIfPresent(TitleLockup.TitleTextColor.self, forKey: .titleColor) { + self.titleColor = titleColor + + } else { + titleColor = .primary + } + + if let color = subTitle?.textColor?.uiColor { + self.subTitleColor = .custom(color, color) + + } else if let subTitleColor = try? typeContainer.decodeIfPresent(TitleLockup.TextColor.self, forKey: .subTitleColor) { + self.subTitleColor = subTitleColor + + } else { + subTitleColor = .primary + } + if let newAlignment = try typeContainer.decodeIfPresent(VDS.TitleLockup.TextAlignment.self, forKey: .alignment) { alignment = newAlignment } @@ -94,20 +131,17 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco try container.encode(moleculeName, forKey: .moleculeName) try container.encode(textAlignment, forKey: .textAlignment) try container.encodeIfPresent(eyebrow, forKey: .eyebrow) + try container.encode(eyebrowColor, forKey: .eyebrowColor) try container.encodeModel(title, forKey: .title) + try container.encode(titleColor, forKey: .titleColor) try container.encodeIfPresent(subTitle, forKey: .subTitle) + try container.encode(subTitleColor, forKey: .subTitleColor) try container.encode(alignment, forKey: .alignment) try container.encode(inverted, forKey: .inverted) } public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.EyebrowModel? { guard let eyebrow else { return nil } - - var eyebrowColor: TitleLockup.TextColor = .primary - if let color = eyebrow.textColor?.uiColor { - eyebrowColor = .custom(color, color) - } - let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = eyebrow.fontStyle { @@ -126,12 +160,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco } public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.TitleModel { - - var titleColor: TitleLockup.TitleTextColor = .primary - if let color = title.textColor?.uiColor { - titleColor = .custom(color, color) - } - let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { if let style = title.fontStyle { @@ -152,12 +180,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.SubTitleModel? { guard let subTitle else { return nil } - - var subTitleColor: TitleLockup.TextColor = .primary - if let color = subTitle.textColor?.uiColor { - subTitleColor = .custom(color, color) - } - let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) do { From e7576ab19b60bcbad944573af1fe0a8429baca41 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 7 May 2024 16:37:06 -0500 Subject: [PATCH 15/64] removed old comments Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift | 1 - .../Molecules/DesignedComponents/LockUps/TitleLockupModel.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index ca90d6ae..383c03c2 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -57,7 +57,6 @@ open class TileletModel: TileContainerBaseModel, Molec textWidth = try container.decodeIfPresent(CGFloat.self, forKey: .textWidth) textPercentage = try container.decodeIfPresent(CGFloat.self, forKey: .textPercentage) - /// look for color hex code if let color = eyebrow?.textColor?.uiColor { self.eyebrowColor = .custom(color, color) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 8296639a..1653ae4b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -82,7 +82,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow) subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle) - /// look for color hex code if let color = eyebrow?.textColor?.uiColor { self.eyebrowColor = .custom(color, color) From 861c6fd4d400f54dee79d28a410d92ff32cd5caf Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Tue, 7 May 2024 18:52:33 -0400 Subject: [PATCH 16/64] Digital ACT191 story ONEAPP-7718 - Bugfix for headlineBody legacy mapping to titleLockup. Remove clipsToBounds to allow shadows. --- .../H1/HeadersH1NoButtonsBodyTextModel.swift | 24 ++++++++++++++----- .../Atomic/Molecules/Items/CarouselItem.swift | 2 +- .../Atomic/Organisms/Carousel/Carousel.swift | 1 + 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift index 97d635a1..b8d5e475 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift @@ -92,19 +92,31 @@ public struct DeprecatedHeadlineBodyHelper { public func createTitleLockupModel(defaultStyle: HeadlineBodyModel.Style = .header, headlineBody: HeadlineBodyModel) throws -> TitleLockupModel { guard let headline = headlineBody.headline else { throw ModelRegistry.Error.decoderOther(message: "headline is required for this use case.") } let body = headlineBody.body + var defaultHeadlineStyle: Styler.Font + var defaultBodyStyle: Styler.Font switch headlineBody.style ?? defaultStyle { case .landingHeader: - headline.fontStyle = Styler.Font.RegularTitle2XLarge - body?.fontStyle = Styler.Font.RegularTitleMedium + defaultHeadlineStyle = Styler.Font.RegularTitle2XLarge + defaultBodyStyle = Styler.Font.RegularTitleMedium case .itemHeader: - headline.fontStyle = Styler.Font.BoldTitleLarge - body?.fontStyle = Styler.Font.RegularBodyLarge + defaultHeadlineStyle = Styler.Font.BoldTitleLarge + defaultBodyStyle = Styler.Font.RegularBodyLarge default: - headline.fontStyle = Styler.Font.RegularTitleXLarge - body?.fontStyle = Styler.Font.RegularTitleMedium + defaultHeadlineStyle = Styler.Font.RegularTitleXLarge + defaultBodyStyle = Styler.Font.RegularTitleMedium + } + if headline.fontStyle == nil { + headline.fontStyle = defaultHeadlineStyle + } + if body?.fontStyle == nil { + body?.fontStyle = defaultBodyStyle } let model = TitleLockupModel(title: headline, subTitle: body) model.id = headlineBody.id + if let textAlignment = headline.textAlignment ?? body?.textAlignment, + textAlignment == .center { + model.textAlignment = .center + } return model } diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift index 301a2274..509c9e61 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift @@ -22,7 +22,7 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { open override func setupView() { super.setupView() - clipsToBounds = true + clipsToBounds = false // Covers the card when peaking. peakingCover.backgroundColor = .white diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 18d65559..914e6427 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -125,6 +125,7 @@ open class Carousel: View { collectionView.dataSource = self collectionView.delegate = self collectionView.bounces = false + collectionView.clipsToBounds = false addSubview(collectionView) bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300) From 91cee993b3db84fb53b3d757c699c97c67652870 Mon Sep 17 00:00:00 2001 From: Nandhini Rajendran Date: Wed, 8 May 2024 08:01:14 +0530 Subject: [PATCH 17/64] CXTDT-552665 Fixing top notification close button --- .../TopNotification/NotificationMoleculeView.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift index 38c58aa9..a0cd0b17 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift @@ -49,7 +49,15 @@ import VDS if let closeButton = viewModel.closeButton { onCloseClick = { [weak self] _ in guard let self else { return } - self.executeAction(model: closeButton, delegateObject: self.delegateObject, additionalData: self.additionalData) } + if closeButton.action.actionType == ActionNoopModel.identifier { + if var dismissAction = self.viewModel.closeButton { + dismissAction.action = ActionDismissNotificationModel() + self.executeAction(model: dismissAction, delegateObject: self.delegateObject, additionalData: self.additionalData) + } + } else { + self.executeAction(model: closeButton, delegateObject: self.delegateObject, additionalData: self.additionalData) + } + } } hideCloseButton = viewModel.closeButton == nil From c628616e4a5c41d9c30f0b26bc1d8327313c5ff8 Mon Sep 17 00:00:00 2001 From: Nandhini Rajendran Date: Wed, 8 May 2024 08:25:54 +0530 Subject: [PATCH 18/64] Remove NotificationXButton class --- MVMCoreUI.xcodeproj/project.pbxproj | 4 -- .../TopNotification/NotificationXButton.swift | 41 ------------------- 2 files changed, 45 deletions(-) delete mode 100644 MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 62792baf..f9443f01 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -558,7 +558,6 @@ D2ED27EE254B0CE700A1C293 /* ActionAlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */; }; D2ED27EF254B0CE700A1C293 /* AlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27EA254B0CE700A1C293 /* AlertModel.swift */; }; D2ED27FC254B0E0300A1C293 /* AlertObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F3254B0E0200A1C293 /* AlertObject.swift */; }; - D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D12513EA6900564112 /* NotificationXButton.swift */; }; D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D32514F80C00564112 /* CollapsableNotification.swift */; }; D2FA83D62515021F00564112 /* CollapsableNotificationTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */; }; D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */; }; @@ -1168,7 +1167,6 @@ D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionAlertModel.swift; sourceTree = ""; }; D2ED27EA254B0CE700A1C293 /* AlertModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertModel.swift; sourceTree = ""; }; D2ED27F3254B0E0200A1C293 /* AlertObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertObject.swift; sourceTree = ""; }; - D2FA83D12513EA6900564112 /* NotificationXButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButton.swift; sourceTree = ""; }; D2FA83D32514F80C00564112 /* CollapsableNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotification.swift; sourceTree = ""; }; D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotificationTopView.swift; sourceTree = ""; }; D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeContainer.swift; sourceTree = ""; }; @@ -2496,7 +2494,6 @@ isa = PBXGroup; children = ( D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */, - D2FA83D12513EA6900564112 /* NotificationXButton.swift */, D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */, D23118B225124E18001C8440 /* NotificationMoleculeView.swift */, D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */, @@ -3063,7 +3060,6 @@ EACCF38C2ABB346700E0F104 /* VDS-Interpreters.swift in Sources */, C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */, 0AE98BB523FF18D2004C5109 /* Arrow.swift in Sources */, - D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */, D2D90B442404789000DD6EC9 /* MoleculeContainerProtocol.swift in Sources */, 0A7ECC5F243CEB1200C828E8 /* ColorViewWithLabel.swift in Sources */, BB3BC12F2550094500297977 /* ListLeftVariableIconWithRightCaretAllTextLinks.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift deleted file mode 100644 index c102d064..00000000 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// NotificationXButton.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 9/17/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - -import Foundation -import MVMCore - -@objcMembers open class NotificationXButton: Button { - - open func closeTopAlert(with delegateObject: MVMCoreUIDelegateObject?) { - MVMCoreUIActionHandler.performActionUnstructured(with: ActionDismissNotificationModel(), sourceModel: model, additionalData: nil, delegateObject: delegateObject) - } - - open override func setupView() { - if let image = MVMCoreUIUtility.imageNamed("nav_close")?.withRenderingMode(.alwaysTemplate) { - setImage(image, for: .normal) - } - tintColor = .white - adjustsImageWhenHighlighted = false - accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "AccCloseButton") - setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - heightAnchor.constraint(equalToConstant: 16.0).isActive = true - widthAnchor.constraint(equalToConstant: 16.0).isActive = true - } - - open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { - super.set(with: model, delegateObject, additionalData) - guard let model = model as? NotificationXButtonModel else { return } - - // TODO: Temporary, consider action for dismissing top alert - if model.action.actionType == ActionNoopModel.identifier { - addActionBlock(event: .touchUpInside) { (button) in - (button as? NotificationXButton)?.closeTopAlert(with: delegateObject) - } - } - } -} From 31096a15a50098a2ae344ff750503e89bf99aca6 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 7 May 2024 23:28:53 -0400 Subject: [PATCH 19/64] Digital PCT265 story ONEAPP-7249 - Prevent UI updates when there are no model changes. --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + .../Atomic/Atoms/Buttons/ButtonModel.swift | 16 ++++ .../Atoms/Buttons/ImageButtonModel.swift | 2 +- .../Atomic/Atoms/Buttons/Link/LinkModel.swift | 13 ++++ .../CarouselIndicatorModel.swift | 16 ++++ .../Label/LabelAttributeActionModel.swift | 5 ++ .../Label/LabelAttributeColorModel.swift | 5 ++ .../Views/Label/LabelAttributeFontModel.swift | 7 ++ .../Label/LabelAttributeImageModel.swift | 8 ++ .../Views/Label/LabelAttributeModel.swift | 6 ++ .../Label/LabelAttributeUnderlineModel.swift | 7 ++ .../Atomic/Atoms/Views/Label/LabelModel.swift | 20 +++++ MVMCoreUI/Atomic/Atoms/Views/LineModel.swift | 8 ++ .../Atoms/Views/TileContainerModel.swift | 2 +- .../Headers/H1/HeadersH1ButtonModel.swift | 10 ++- .../H1/HeadersH1LandingPageHeaderModel.swift | 18 +++-- .../H1/HeadersH1NoButtonsBodyTextModel.swift | 2 +- .../Headers/H2/HeadersH2ButtonsModel.swift | 10 ++- .../Headers/H2/HeadersH2CaretLinkModel.swift | 10 ++- .../Headers/H2/HeadersH2LinkModel.swift | 12 ++- .../H2/HeadersH2NoButtonsBodyTextModel.swift | 2 +- .../H2/HeadersH2PricingTwoRowsModel.swift | 22 +++--- .../Headers/H2/HeadersH2TinyButtonModel.swift | 10 ++- ...istLeftVariableCheckboxBodyTextModel.swift | 10 ++- ...istLeftVariableIconAllTextLinksModel.swift | 10 ++- ...eIconWithRightCaretAllTextLinksModel.swift | 12 ++- ...iableIconWithRightCaretBodyTextModel.swift | 12 ++- ...tLeftVariableIconWithRightCaretModel.swift | 12 ++- ...LeftVariableRadioButtonBodyTextModel.swift | 10 ++- ...tOneColumnFullWidthTextBodyTextModel.swift | 7 +- ...htVariableButtonAllTextAndLinksModel.swift | 10 ++- ...riableRightCaretAlltextAndLinksModel.swift | 10 ++- .../LockUps/TitleLockupModel.swift | 24 ++++-- ...nFullWidthTextDividerSubsectionModel.swift | 10 ++- ...nTextWithWhitespaceDividerShortModel.swift | 10 ++- ...mnTextWithWhitespaceDividerTallModel.swift | 10 ++- .../TwoButtonViewModel.swift | 10 ++- .../Molecules/Items/CarouselItemModel.swift | 11 +++ .../Molecules/Items/ListItemModel.swift | 1 + .../Items/MoleculeCollectionItemModel.swift | 6 ++ .../Items/MoleculeListItemModel.swift | 14 ++++ .../Items/MoleculeStackItemModel.swift | 8 ++ .../Molecules/Items/StackItemModel.swift | 8 ++ .../LeftRightViews/CornerLabelsModel.swift | 16 ++-- .../MoleculeContainerModel.swift | 8 +- .../MoleculeContainerProtocol.swift | 2 +- .../EyebrowHeadlineBodyLinkModel.swift | 22 ++++-- .../HeadlineBodyModel.swift | 18 ++++- .../Organisms/Carousel/CarouselModel.swift | 25 +++++- MVMCoreUI/Atomic/Organisms/StackModel.swift | 7 ++ .../Atomic/Organisms/StackModelProtocol.swift | 2 +- .../MoleculeComparisonProtocol.swift | 16 ++++ .../MoleculeModelProtocol.swift | 11 ++- .../ParentMoleculeModelProtocol.swift | 76 +++++++++++++++---- .../TemplateModelProtocol.swift | 14 ++-- .../Atomic/Templates/BaseTemplateModel.swift | 8 +- .../Templates/CollectionTemplateModel.swift | 11 ++- .../Templates/ListPageTemplateModel.swift | 14 +++- .../Templates/StackPageTemplateModel.swift | 10 ++- .../Templates/ThreeLayerModelBase.swift | 14 +++- .../ThreeLayerPageTemplateModel.swift | 12 ++- .../BaseControllers/ViewController.swift | 31 +++++--- MVMCoreUI/CustomPrimitives/Color.swift | 7 ++ 63 files changed, 589 insertions(+), 155 deletions(-) diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 62792baf..36641951 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ 5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */; }; 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; }; 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; }; + 58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */; }; 608211282AC6B57E00C3FC39 /* MVMCoreUILoggingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; }; 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; }; @@ -780,6 +781,7 @@ 5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = ""; }; 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = ""; }; 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceableMoleculeBehaviorModel.swift; sourceTree = ""; }; + 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeComparisonProtocol.swift; sourceTree = ""; }; 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingHandler.swift; sourceTree = ""; }; 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = ""; }; 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = ""; }; @@ -1254,6 +1256,7 @@ D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */, 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */, 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */, + 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */, ); path = ModelProtocols; sourceTree = ""; @@ -3010,6 +3013,7 @@ EA1758482BC97ED800A5C0D9 /* BadgeIndicator.swift in Sources */, 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */, + 58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */, D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */, AF1C336928859778006B1001 /* ActionAlertHandler.swift in Sources */, 9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index dec3480c..6812424b 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -186,4 +186,20 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) try container.encodeIfPresent(disabledAccessibilityTraits, forKey: .disabledAccessibilityTraits) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return title == model.title + && enabled == model.enabled + && inverted == model.inverted + && action.isEqual(to: model.action) + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && style == model.style + && size == model.size + && groupName == model.groupName + && width == model.width + && accessibilityTraits == model.accessibilityTraits + && disabledAccessibilityTraits == model.disabledAccessibilityTraits + } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift index 6d293a34..134a42b9 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift @@ -33,7 +33,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro [image].compactMap({$0}) } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &image, with: molecule) } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift index 14e041e5..38cbe673 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -99,6 +99,19 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode try container.encodeIfPresent(size, forKey: .size) try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && title == model.title + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && inverted == model.inverted + && enabled == model.enabled + && size == model.size + && shouldMaskRecordedView == model.shouldMaskRecordedView +// && action.isEqual(to: model.action) // TODO: Move to isVisiuallyEquivalent. + } } extension LinkModel { diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 3631bfc0..1bc399bb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -130,4 +130,20 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro try container.encode(indicatorColor, forKey: .indicatorColor) try container.encodeIfPresent(position, forKey: .position) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && numberOfPages == model.numberOfPages + && currentIndex == model.currentIndex + && alwaysSendAction == model.alwaysSendAction + && animated == model.animated + && hidesForSinglePage == model.hidesForSinglePage + && accessibilityHasSlidesInsteadOfPage == model.accessibilityHasSlidesInsteadOfPage + && enabled == model.enabled + && inverted == model.inverted + && disabledIndicatorColor == model.disabledIndicatorColor + && indicatorColor == model.indicatorColor + && position == model.position + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift index 1ecbe874..e65af621 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift @@ -48,4 +48,9 @@ open class LabelAttributeActionModel: LabelAttributeModel { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeModel(action, forKey: .action) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return action.isEqual(to: model.action) + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift index 8fa1aa58..68a59d4d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift @@ -43,4 +43,9 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(textColor, forKey: .textColor) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return textColor == model.textColor + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift index 6a2a1af5..931c0d63 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift @@ -55,4 +55,11 @@ try container.encodeIfPresent(name, forKey: .name) try container.encodeIfPresent(size, forKey: .size) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return style == model.style + && name == model.name + && size == model.size + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift index 03d0ff08..0dceaf7d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift @@ -69,4 +69,12 @@ class LabelAttributeImageModel: LabelAttributeModel { try container.encodeIfPresent(URL, forKey: .URL) try container.encodeIfPresent(tintColor, forKey: .tintColor) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return URL == model.URL + && name == model.name + && size == model.size + && tintColor == model.tintColor + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index 88192720..35ad5700 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -75,4 +75,10 @@ try container.encode(location, forKey: .location) try container.encode(length, forKey: .length) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return location == model.location + && length == model.length + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift index f3578418..b2a226e5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift @@ -66,6 +66,13 @@ import UIKit try container.encode(style, forKey: .style) try container.encodeIfPresent(pattern, forKey: .pattern) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return style == model.style + && color == model.color + && pattern == model.pattern + } } public enum UnderlineStyle: String, Codable { diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index fe37a2c0..d3611a2e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -132,6 +132,26 @@ import VDS try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && text == model.text + && accessibilityText == model.accessibilityText + && textColor == model.textColor + && fontStyle == model.fontStyle + && fontName == model.fontName + && fontSize == model.fontSize + && textAlignment == model.textAlignment + && attributes.areEqual(to: model.attributes) + && html == model.html + && hero == model.hero + && makeWholeViewClickable == model.makeWholeViewClickable + && numberOfLines == model.numberOfLines + && shouldMaskRecordedView == model.shouldMaskRecordedView + && accessibilityTraits == model.accessibilityTraits + && inverted == inverted + } } extension LabelModel { diff --git a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift index 2cfe9b99..7c3e66ec 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift @@ -129,4 +129,12 @@ public class LineModel: MoleculeModelProtocol, Invertable { try container.encodeIfPresent(frequency, forKey: .frequency) try container.encode(orientation == .vertical, forKey: .useVerticalLine) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return type == model.type + && inverted == model.inverted + && frequency == model.frequency + && orientation == model.orientation + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift index 895bb027..c60ac2ef 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift @@ -24,7 +24,7 @@ open class TileContainerModel: TileContainerBaseModel Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &self.molecule, with: molecule) } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift index 0cf8410f..901cf057 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift @@ -21,9 +21,13 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMol [titleLockup, buttons] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &titleLockup, with: molecule) - || replaceChildMolecule(at: &buttons, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift index 651a7f89..995cb482 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift @@ -24,13 +24,17 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol [headline, headline2, subHeadline, body, link, buttons] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &headline2, with: molecule) - || replaceChildMolecule(at: &subHeadline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) - || replaceChildMolecule(at: &link, with: molecule) - || replaceChildMolecule(at: &buttons, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &headline2, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &subHeadline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &link, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift index 7450c9aa..9fec7e5b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift @@ -19,7 +19,7 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol [titleLockup] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &titleLockup, with: molecule) } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift index 9590fefc..6d9bef90 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift @@ -23,9 +23,13 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMo [titleLockup, buttons] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &titleLockup, with: molecule) - || replaceChildMolecule(at: &buttons, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift index 784966cf..1cb89f84 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift @@ -20,9 +20,13 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, Parent [titleLockup, caretLink] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &titleLockup, with: molecule) - || replaceChildMolecule(at: &caretLink, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &caretLink, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift index 0a104f9f..8ff7df5e 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift @@ -8,7 +8,7 @@ import Foundation -public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { +public class HeadersH2LinkModel: HeaderModel, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -22,9 +22,13 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMolec [titleLockup, link] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &titleLockup, with: molecule) - || replaceChildMolecule(at: &link, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &link, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift index 671e5c0b..e18aafe6 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift @@ -22,7 +22,7 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol [titleLockup] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &titleLockup, with: molecule) } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift index 54a62b5b..b92e04d1 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift @@ -25,15 +25,19 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P [headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0}) } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) - || replaceChildMolecule(at: &subBody, with: molecule) - || replaceChildMolecule(at: &body2, with: molecule) - || replaceChildMolecule(at: &body2, with: molecule) - || replaceChildMolecule(at: &subBody2, with: molecule) - || replaceChildMolecule(at: &body3, with: molecule) - || replaceChildMolecule(at: &subBody3, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &subBody, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body2, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body2, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &subBody2, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body3, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &subBody3, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift index aba4e48b..a13c6b9e 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift @@ -23,9 +23,13 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, Paren [titleLockup, button] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &titleLockup, with: molecule) - || replaceChildMolecule(at: &button, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &button, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift index ba7d4986..6e543477 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift @@ -20,9 +20,13 @@ open class ListLeftVariableCheckboxBodyTextModel: ListItemModel, MoleculeModelPr [checkbox, headlineBody] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &checkbox, with: molecule) - || replaceChildMolecule(at: &headlineBody, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &checkbox, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &headlineBody, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift index c7a32e78..1a18f676 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift @@ -20,9 +20,13 @@ public class ListLeftVariableIconAllTextLinksModel: ListItemModel, MoleculeModel return [image, eyebrowHeadlineBodyLink] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &image, with: molecule) - || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift index 9d5ce50b..af465e48 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift @@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretAllTextLinksModel: ListItemModel, return [image, eyebrowHeadlineBodyLink, rightLabel] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &image, with: molecule) - || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) - || replaceChildMolecule(at: &rightLabel, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift index 6b95c669..4bd6c0ed 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift @@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretBodyTextModel: ListItemModel, Par [image, headlineBody, rightLabel] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &image, with: molecule) - || replaceChildMolecule(at: &headlineBody, with: molecule) - || replaceChildMolecule(at: &rightLabel, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &headlineBody, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift index a804b8d4..08d70edb 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift @@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretModel: ListItemModel, ParentMolec return [image, leftLabel, rightLabel] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &image, with: molecule) - || replaceChildMolecule(at: &leftLabel, with: molecule) - || replaceChildMolecule(at: &rightLabel, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &leftLabel, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift index 808ee19e..167574cb 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift @@ -20,9 +20,13 @@ open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, ParentMolecu [radioButton, headlineBody] } - public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &radioButton, with: replacementMolecule) - || replaceChildMolecule(at: &headlineBody, with: replacementMolecule) + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &radioButton, with: replacementMolecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &headlineBody, with: replacementMolecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift index aee85f74..3e4c66bf 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift @@ -39,7 +39,7 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod return [headlineBody] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &headlineBody, with: molecule) } @@ -68,4 +68,9 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod try container.encode(moleculeName, forKey: .moleculeName) try container.encode(headlineBody, forKey: .headlineBody) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return headlineBody.isEqual(to: headlineBody) + } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift index 3d68a8ff..bcace0b3 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift @@ -40,9 +40,13 @@ public class ListRightVariableButtonAllTextAndLinksModel: ListItemModel, Molecul return [button, eyebrowHeadlineBodyLink] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &button, with: molecule) - || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &button, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift index 94d65c37..ca777e98 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift @@ -20,9 +20,13 @@ public class ListRightVariableRightCaretAllTextAndLinksModel: ListItemModel, Par [rightLabel, eyebrowHeadlineBodyLink] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &rightLabel, with: molecule) - || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index a280e0b4..a9ae8602 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -9,7 +9,7 @@ import VDSTokens import VDS -public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { +public class TitleLockupModel: ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties @@ -34,10 +34,24 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco [eyebrow, title, subTitle].compactMap { (molecule: MoleculeModelProtocol?) in molecule } } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &eyebrow, with: molecule) - || replaceChildMolecule(at: &title, with: molecule) - || replaceChildMolecule(at: &subTitle, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &eyebrow, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &title, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &subTitle, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil + } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return textAlignment == model.textAlignment + && subTitleColor == model.subTitleColor + && alignment == model.alignment + && inverted == model.inverted + && backgroundColor == model.backgroundColor + && children.areEqual(to: model.children) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift index af226405..6fea470f 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift @@ -22,9 +22,13 @@ public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, Mo [headline, body].compactMap({$0}) } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift index 596b9ff0..141641e5 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift @@ -22,9 +22,13 @@ public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, Mo [headline, body].compactMap({$0}) } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift index ea4c3ce2..74fd2965 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift @@ -22,9 +22,13 @@ public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, Mol [headline, body].compactMap({$0}) } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 0ed1db11..dc471e0a 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -23,9 +23,13 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { public var children: [MoleculeModelProtocol] { [primaryButton, secondaryButton].compactMap { $0 } } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &primaryButton, with: molecule) - || replaceChildMolecule(at: &secondaryButton, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &primaryButton, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &secondaryButton, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index 79fe22ab..c412b09c 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -85,8 +85,19 @@ try container.encodeIfPresent(peakingUI, forKey: .peakingUI) try container.encodeIfPresent(peakingArrowColor, forKey: .peakingArrowColor) try container.encodeIfPresent(analyticsData, forKey: .analyticsData) + try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encode(enabled, forKey: .enabled) try container.encode(readOnly, forKey: .readOnly) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return peakingUI == model.peakingUI + && peakingArrowColor == model.peakingArrowColor + && analyticsData == model.analyticsData + && fieldValue == model.fieldValue + && enabled == model.enabled + && readOnly == model.readOnly + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index aea87731..b156743d 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -9,6 +9,7 @@ import MVMCore @objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 2afda5a4..383c4692 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -73,4 +73,10 @@ try container.encodeIfPresent(border, forKey: .border) try super.encode(to: encoder) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return action.isEqual(to: model.action) + && border == border + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift index 9e230607..48b4fc2a 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift @@ -52,4 +52,18 @@ import MVMCore try container.encode(moleculeName, forKey: .moleculeName) try container.encodeModel(molecule, forKey: .molecule) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && action.isEqual(to: model.action) + && hideArrow == model.hideArrow + && line.isEqual(to: model.line) + && style == model.style + && gone == model.gone + && molecule.isEqual(to: model.molecule) + && accessibilityTraits == model.accessibilityTraits + && accessibilityValue == model.accessibilityValue + && accessibilityText == model.accessibilityText + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift index b5c42984..75dcfd4f 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift @@ -58,4 +58,12 @@ try container.encodeIfPresent(percent, forKey: .percent) try container.encode(gone, forKey: .gone) } + + public override func isEqual(to model: any ModelProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && spacing == model.spacing + && percent == model.percent + && gone == model.gone + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift index 6fa8b6fa..bd496f3a 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift @@ -36,4 +36,12 @@ required public init(from decoder: Decoder) throws { fatalError("init(from:) has not been implemented") } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && spacing == model.spacing + && percent == model.percent + && gone == model.gone + } } diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift index 4925d03b..ad45061e 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift @@ -23,12 +23,16 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol { [molecule, topLeftLabel, topRightLabel, bottomLeftLabel, bottomRightLabel].compactMap { $0 } } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &self.molecule, with: molecule) - || replaceChildMolecule(at: &topLeftLabel, with: molecule) - || replaceChildMolecule(at: &topRightLabel, with: molecule) - || replaceChildMolecule(at: &bottomLeftLabel, with: molecule) - || replaceChildMolecule(at: &bottomRightLabel, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &self.molecule, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &topLeftLabel, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &topRightLabel, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &bottomLeftLabel, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &bottomRightLabel, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } public init(with molecule: MoleculeModelProtocol?) { diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index 6860b40a..ce271ba6 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -20,7 +20,7 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco return [molecule] } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &self.molecule, with: molecule) } @@ -61,4 +61,10 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco try container.encodeModel(molecule, forKey: .molecule) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return molecule.isEqual(to: model.molecule) + && backgroundColor == model.backgroundColor + } } diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift index cbda8dde..5c6deee7 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift @@ -19,7 +19,7 @@ public extension MoleculeContainerModelProtocol { } public extension MoleculeContainerModelProtocol where Self: AnyObject { - mutating func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + mutating func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(at: &molecule, with: replacementMolecule) } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index 90f72be3..df7b897e 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -7,7 +7,7 @@ // -public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { +public class EyebrowHeadlineBodyLinkModel: ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -25,11 +25,15 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule [eyebrow, headline, body, link].compactMap { (molecule: MoleculeModelProtocol?) in molecule } } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &eyebrow, with: molecule) - || replaceChildMolecule(at: &headline, with: molecule) - || replaceChildMolecule(at: &body, with: molecule) - || replaceChildMolecule(at: &link, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &eyebrow, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &link, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- @@ -102,4 +106,10 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule try container.encodeIfPresent(body, forKey: .body) try container.encodeIfPresent(link, forKey: .link) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && children.areEqual(to: model.children) + } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index 093528d1..b8e62030 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -24,9 +24,13 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { [headline, body].compactMap { $0 } } - public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at:&headline, with: replacementMolecule) - || replaceChildMolecule(at:&body, with: replacementMolecule) + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at:&headline, with: replacementMolecule, replaced: &replacedMolecule) + || replaceChildMolecule(at:&body, with: replacementMolecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- @@ -89,6 +93,14 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(style, forKey: .style) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return headline.isEqual(to: model.headline) + && body.isEqual(to: model.body) + && style == style + && backgroundColor == backgroundColor + } } public extension HeadlineBodyModel { diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 976018dd..4e645a92 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -171,6 +171,29 @@ import UIKit try container.encode(enabled, forKey: .enabled) try container.encode(readOnly, forKey: .readOnly) } + + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == backgroundColor + && molecules.areEqual(to: model.molecules) + && spacing == model.spacing + && border == model.border + && loop == model.loop + && height == model.height + && itemWidthPercent == model.itemWidthPercent + && itemAlignment == model.itemAlignment + && pagingMolecule.isEqual(to: model.pagingMolecule) + && paging == model.paging + && useHorizontalMargins == model.useHorizontalMargins + && leftPadding == model.leftPadding + && rightPadding == model.rightPadding + && accessibilityText == model.accessibilityText + && baseValue == model.baseValue + && fieldKey == model.fieldKey + && groupName == model.groupName + && enabled == model.enabled + && readOnly == model.readOnly + } } extension CarouselModel { @@ -179,7 +202,7 @@ extension CarouselModel { return molecules } - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(in: &molecules, with: molecule) } diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index bc254727..7f03df14 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -79,4 +79,11 @@ try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + public func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && molecules.areEqual(to: model.molecules) + && axis == model.axis + && spacing == model.spacing + } } diff --git a/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift b/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift index 1456f6e5..3bff88fc 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift @@ -21,7 +21,7 @@ extension StackModelProtocol { extension StackModelProtocol where Self: AnyObject { - public mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return try replaceChildMolecule(in: &molecules, with: molecule) } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index f2ac0715..907c2cfa 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -7,3 +7,19 @@ // import Foundation + +public protocol MoleculeModelComparisonProtocol: ModelProtocol { + + /** True if there are no visual differences between models. + + By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. + **/ + func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol) -> Bool +} + +extension MoleculeModelComparisonProtocol { + + public func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol) -> Bool { + return isEqual(to: model) + } +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index e56840e8..c355d3f6 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -5,7 +5,7 @@ public enum MolecularError: Swift.Error { case countImbalance(String) } -public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol { +public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol, MoleculeModelComparisonProtocol, CustomDebugStringConvertible { var moleculeName: String { get } var backgroundColor: Color? { get set } var id: String { get } @@ -18,6 +18,15 @@ public extension MoleculeModelProtocol { static var categoryName: String { "\(MoleculeModelProtocol.self)" } static var categoryCodingKey: String { "moleculeName" } + + func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return id == model.id + } + + var debugDescription: String { + return "\(moleculeName): \(id)" + } } // Helpers made due to swift not able to reconcile which category. diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index ef53b177..88817e6a 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -8,52 +8,70 @@ import Foundation -public protocol ParentModelProtocol: MoleculeTreeTraversalProtocol { +public protocol ParentModelProtocol: ModelProtocol, MoleculeTreeTraversalProtocol { /// Returns the direct children of this component. (Does not recurse.) var children: [MoleculeModelProtocol] { get } /// Method for replacing surface level children. (Does not recurse.) - mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool + mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? } public extension ParentModelProtocol where Self: AnyObject { - + /// Top level test to replace child molecules. Each parent molecule should attempt to replace. - func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { return false } + func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { return nil } + + func replaceChildMolecule(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + return try replaceChildMolecule(at: &childMolecule, with: replacementMolecule, replaced: &replacedMolecule) ? replacedMolecule : nil + } /// Helper function for replacing a single molecules with type and optionality checks. - func replaceChildMolecule(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + func replaceChildMolecule(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol, replaced: inout MoleculeModelProtocol?) throws -> Bool { guard let childIdMolecule = childMolecule as? MoleculeModelProtocol else { return false } if childIdMolecule.id == replacementMolecule.id { - guard let replacementMolecule = replacementMolecule as? T else { + guard let typedReplacementMolecule = replacementMolecule as? T else { throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))") } - childMolecule = replacementMolecule + replaced = childIdMolecule + childMolecule = typedReplacementMolecule return true } return false } + 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 + } + /// Helper for replacing a molecule in place within an array. Note the "in". - func replaceChildMolecule(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol) throws -> Bool { - guard let moleculeIdModels = molecules as? [MoleculeModelProtocol], let matchingIndex = moleculeIdModels.firstIndex(where: { - $0.id == replacementMolecule.id - }) else { return false } - guard let replacementMolecule = replacementMolecule as? T else { + func replaceChildMolecule(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol, replaced: inout MoleculeModelProtocol?) throws -> Bool { + guard let moleculeIdModels = molecules as? [MoleculeModelProtocol], + let matchingIndex = moleculeIdModels.firstIndex(where: { + $0.id == replacementMolecule.id + }) + else { return false } + guard let typedReplacementMolecule = replacementMolecule as? T else { throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))") } - molecules[matchingIndex] = replacementMolecule + replaced = molecules[matchingIndex] as? MoleculeModelProtocol + molecules[matchingIndex] = typedReplacementMolecule return true } } public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelProtocol { - } public extension ParentMoleculeModelProtocol { + func isEqual(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.children.areEqual(to: model.children) + } + func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int) -> Result) -> Result { var result = initialResult if (options == .parentFirst) { @@ -64,7 +82,7 @@ public extension ParentMoleculeModelProtocol { // Safety net to make sure the ParentMoleculeModelProtocol's method extension is called over the base MoleculeModelProtocol. return additionalParent.reduceDepthFirstTraverse(options: options, depth: depth + 1, initialResult: result, nextPartialResult: nextPartialResult) } - return molecule.reduceDepthFirstTraverse(options: options, depth: depth + 1, initialResult: result, nextPartialResult: nextPartialResult) + return nextPartialResult(result, molecule, depth + 1) } if (options == .childFirst) { result = nextPartialResult(result, self, depth) @@ -88,7 +106,7 @@ public extension ParentMoleculeModelProtocol { // Safety net to make sure the ParentMoleculeModelProtocol's method extension is called over the base MoleculeModelProtocol. additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept) } else { - child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept) + onVisit(depth, child, &shouldStop) } guard !shouldStop else { return } } @@ -98,3 +116,29 @@ public extension ParentMoleculeModelProtocol { // if options == .leafOnly don't call on self. } } + +extension ParentModelProtocol { + + func deepCompare(_ anotherParent: ParentModelProtocol, with test: (ModelProtocol, ModelProtocol)->Bool) -> (Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?) { + + guard test(self, anotherParent) else { return (false, myChild: self, theirChild: self)} + + let myChildren = children + let theirChildren = anotherParent.children + guard myChildren.count == theirChildren.count else { return (false, myChild: self, theirChild: self) } + 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) + guard result.0 else { return result } + } else { + return (false, myChild: myChild, theirChild: theirChildren[index]) + } + } else if !test(myChildren[index], theirChildren[index]) { + return (false, myChild: myChildren[index], theirChild: theirChildren[index]) + } + } + + return (true, nil, nil) + } +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 296f73f5..5507eadb 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -42,27 +42,27 @@ public extension TemplateModelProtocol { extension TemplateModelProtocol { /// Recursively finds and replaces the first child matching the replacement molecule id property. - mutating func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + mutating func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { // Attempt root level replacement on the template model first. - if try replaceChildMolecule(with: replacementMolecule) { - return true + if let replacedMolecule = try replaceChildMolecule(with: replacementMolecule) { + return replacedMolecule } - var didReplaceMolecule = false + 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 { - didReplaceMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) + replacedMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) } catch { possibleError = error } - stop = didReplaceMolecule || possibleError != nil + stop = replacedMolecule != nil || possibleError != nil } if let error = possibleError { throw error } - return didReplaceMolecule + return replacedMolecule } } diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index c42797bc..8700fed1 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -35,8 +35,12 @@ import Foundation public var hideLeftPanel: Bool? public var hideRightPanel: Bool? - public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try replaceChildMolecule(at: &navigationBar, with: molecule) + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &navigationBar, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift index e94f8153..c0bb85b8 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift @@ -23,9 +23,14 @@ return super.rootMolecules } - public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try super.replaceChildMolecule(with: molecule) - || (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + if let replacedMolecule = try super.replaceChildMolecule(with: molecule) { + return replacedMolecule + } + if molecules != nil, let replacedMolecule = try replaceChildMolecule(in: &(molecules!), with: molecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift index daf022a8..5671013b 100644 --- a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift @@ -28,10 +28,16 @@ return super.rootMolecules } - public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try super.replaceChildMolecule(with: molecule) - || (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) - || replaceChildMolecule(at: &line, with: molecule) + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + if let replacedMolecule = try super.replaceChildMolecule(with: molecule) { + return replacedMolecule + } + var replacedMolecule: MoleculeModelProtocol? + if try (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule, replaced: &replacedMolecule)) + || replaceChildMolecule(at: &line, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } /// This template requires content. diff --git a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift index d5d783d3..7035ab75 100644 --- a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift @@ -19,10 +19,14 @@ super.rootMolecules + [moleculeStack] } - public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + var replacedMolecule: MoleculeModelProtocol? return try super.replaceChildMolecule(with: molecule) - || replaceChildMolecule(at: &navigationBar, with: molecule) - || replaceChildMolecule(at: &moleculeStack, with: molecule) + if try replaceChildMolecule(at: &navigationBar, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &moleculeStack, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift index 6ca05550..921e74ab 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift @@ -21,10 +21,16 @@ [navigationBar, header, footer].compactMap { $0 } } - public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try super.replaceChildMolecule(with: molecule) - || replaceChildMolecule(at: &header, with: molecule) - || replaceChildMolecule(at: &footer, with: molecule) + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + if let replacedMolecule = try super.replaceChildMolecule(with: molecule) { + return replacedMolecule + } + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &header, with: molecule, replaced: &replacedMolecule) + || replaceChildMolecule(at: &footer, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift index d3acea86..068f2cab 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift @@ -22,9 +22,15 @@ return super.rootMolecules } - public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { - return try super.replaceChildMolecule(with: molecule) - || replaceChildMolecule(at: &middle, with: molecule) + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + if let replacedMolecule = try super.replaceChildMolecule(with: molecule) { + return replacedMolecule + } + var replacedMolecule: MoleculeModelProtocol? + if try replaceChildMolecule(at: &middle, with: molecule, replaced: &replacedMolecule) { + return replacedMolecule + } + return nil } //-------------------------------------------------- diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 452d2962..ba38ad29 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -513,27 +513,34 @@ import MVMCore public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) { pageUpdateQueue.addOperation { - let replacedModels:[MoleculeModelProtocol] = moleculeModels.compactMap { model in - guard self.attemptToReplace(with: model) else { + let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in + guard let replacedMolecule = self.attemptToReplace(with: model) else { return nil } - return model + return (model, replacedMolecule) } - if replacedModels.count > 0 { + let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in + guard !new.isVisuallyEquivalent(to: existing) else { + return nil + } + return new + } + if uiUpdatedModels.count > 0 { + MVMCoreLoggingHandler.shared()?.handleDebugMessage("Updating UI for molecules: \(uiUpdatedModels)") DispatchQueue.main.sync { - self.updateUI(for: replacedModels) + self.updateUI(for: uiUpdatedModels) } } - completionHandler?(replacedModels) + completionHandler?(replacedModels.map { $0.0 }) } } - open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> Bool { - guard var templateModel = getTemplateModel() else { return false } - var didReplace = false + open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> MoleculeModelProtocol? { + guard var templateModel = getTemplateModel() else { return nil } + var replacedMolecule: MoleculeModelProtocol? do { - didReplace = try templateModel.replaceMolecule(with: replacementModel) - if !didReplace { + replacedMolecule = try templateModel.replaceMolecule(with: replacementModel) + if replacedMolecule == nil { MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(replacementModel.id)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!) } } catch { @@ -543,7 +550,7 @@ import MVMCore } MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) } - return didReplace + return replacedMolecule } //-------------------------------------------------- diff --git a/MVMCoreUI/CustomPrimitives/Color.swift b/MVMCoreUI/CustomPrimitives/Color.swift index 9245153d..a67f8747 100644 --- a/MVMCoreUI/CustomPrimitives/Color.swift +++ b/MVMCoreUI/CustomPrimitives/Color.swift @@ -14,6 +14,7 @@ import UIKit Int and String and can be used the same. */ public final class Color: Codable { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -151,3 +152,9 @@ public final class Color: Codable { } } } + +extension Color: Equatable { + public static func == (lhs: Color, rhs: Color) -> Bool { + return lhs.hex == rhs.hex + } +} From 283fc2c17f183e1ccbc84d666b6e4ddd2edf0cc4 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 7 May 2024 23:29:25 -0400 Subject: [PATCH 20/64] Digital PCT265 story ONEAPP-7249 - Relax over aggressive polling on model updates. --- MVMCoreUI/Behaviors/PollingBehaviorModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index 5e1058d8..b8e2eee0 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -86,8 +86,8 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTran public func onPageNew(rootMolecules: [any MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { 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. - resumePollingTimer(withRemainingTime: refreshOnShown ? 0 : remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval) + // 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) } } From b6ac373e0faf1f98f2265295187d44c3a936a393 Mon Sep 17 00:00:00 2001 From: Nandhini Rajendran Date: Wed, 8 May 2024 21:01:07 +0530 Subject: [PATCH 21/64] Addressing review comments. --- .../TopNotification/NotificationMoleculeView.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift index a0cd0b17..414d3dfa 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift @@ -46,14 +46,12 @@ import VDS self.accessibilityIdentifier = accessibilityIdentifier } - if let closeButton = viewModel.closeButton { + if var closeButton = viewModel.closeButton { onCloseClick = { [weak self] _ in guard let self else { return } if closeButton.action.actionType == ActionNoopModel.identifier { - if var dismissAction = self.viewModel.closeButton { - dismissAction.action = ActionDismissNotificationModel() - self.executeAction(model: dismissAction, delegateObject: self.delegateObject, additionalData: self.additionalData) - } + closeButton.action = ActionDismissNotificationModel() + self.executeAction(model: closeButton, delegateObject: self.delegateObject, additionalData: self.additionalData) } else { self.executeAction(model: closeButton, delegateObject: self.delegateObject, additionalData: self.additionalData) } From 067f63e6de3f3f502852e5e60a627efab4b53e4c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 8 May 2024 17:49:33 -0400 Subject: [PATCH 22/64] Digital PCT265 story ONEAPP-7249 - Fix notification retain cycle. --- .../BaseControllers/ViewController.swift | 4 +++- .../Behaviors/PollingBehaviorModel.swift | 1 + .../ReplaceableMoleculeBehaviorModel.swift | 24 ++++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index ba38ad29..c146afa0 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -65,7 +65,9 @@ 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, using: responseJSONUpdated(notification:)) + observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in + self?.responseJSONUpdated(notification: notification) + } } open func stopObservingForResponseJSONUpdates() { diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index b8e2eee0..0d40f4cd 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -137,6 +137,7 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior, PageMoleculeTran } deinit { + debugLog("deinit") pollTimer?.cancel() } } diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 233efdcc..08f9c2f9 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -15,7 +15,16 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol { public var moleculeIds: [String] } -public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { +public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging { + + public var loggingPrefix: String { + "\(self) \(ObjectIdentifier(self))\n\(moleculeIds)\n" + } + + public static var loggingCategory: String? { + String(describing: Self.self) + } + var moleculeIds: [String] var modulesToListenFor: [String] private var observingForResponses: NSObjectProtocol? @@ -34,9 +43,11 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { 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") self.delegateObject = delegateObject let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil if shouldListenForListUpdates { @@ -70,7 +81,9 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { let pageUpdateQueue = OperationQueue() pageUpdateQueue.maxConcurrentOperationCount = 1 pageUpdateQueue.qualityOfService = .userInteractive - observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:)) + observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in + self?.responseJSONUpdated(notification: notification) + } } private func stopListeningForModuleUpdates() { @@ -97,7 +110,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { guard modules.count > 0 else { return } #if LOGGING let requestParams = (notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters - MVMCoreLoggingHandler.shared()?.handleDebugMessage("Replacing \(modules.map { $0.id }) from \(requestParams?.url?.absoluteString ?? "unknown"), e2eId: \(requestParams?.identifier ?? "unknown")") + 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) } @@ -113,4 +126,9 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { } return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as! MoleculeModelProtocol } + + deinit { + debugLog("deinit") + stopListeningForModuleUpdates() + } } From 04581558e355bc1be0056ed37d7dd02e6dcfb0bd Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 8 May 2024 20:34:08 -0400 Subject: [PATCH 23/64] Digital PCT265 story ONEAPP-7249 - isVisuallyEquivalent build out to work with stabilizing carousel refreshes. --- .../Atomic/Atoms/Buttons/ButtonModel.swift | 16 ++++++- .../Atomic/Atoms/Buttons/Link/LinkModel.swift | 15 ++++++- .../CarouselIndicatorModel.swift | 7 ++- .../Label/LabelAttributeActionModel.swift | 6 ++- .../Label/LabelAttributeColorModel.swift | 2 +- .../Views/Label/LabelAttributeFontModel.swift | 2 +- .../Label/LabelAttributeImageModel.swift | 2 +- .../Views/Label/LabelAttributeModel.swift | 4 +- .../Label/LabelAttributeUnderlineModel.swift | 2 +- .../Atomic/Atoms/Views/Label/LabelModel.swift | 23 +++++++++- MVMCoreUI/Atomic/Atoms/Views/LineModel.swift | 2 +- ...tOneColumnFullWidthTextBodyTextModel.swift | 5 --- .../LockUps/TitleLockupModel.swift | 14 +++++- .../Molecules/Items/CarouselItemModel.swift | 10 ++++- .../Molecules/Items/ListItemModel.swift | 28 +++++++++++- .../Items/MoleculeCollectionItemModel.swift | 11 +++-- .../Items/MoleculeListItemModel.swift | 20 ++++----- .../Items/MoleculeStackItemModel.swift | 12 ++++-- .../Items/MoleculeTableViewCell.swift | 5 ++- .../Molecules/Items/StackItemModel.swift | 2 +- .../MoleculeContainerModel.swift | 7 ++- .../EyebrowHeadlineBodyLinkModel.swift | 4 +- .../HeadlineBodyModel.swift | 2 +- .../Atomic/Organisms/Carousel/Carousel.swift | 10 +++++ .../Organisms/Carousel/CarouselModel.swift | 25 ++++++++++- MVMCoreUI/Atomic/Organisms/StackModel.swift | 4 +- .../MoleculeComparisonProtocol.swift | 43 +++++++++++++++++-- .../MoleculeModelProtocol.swift | 2 +- .../ParentMoleculeModelProtocol.swift | 16 ++++++- .../BaseControllers/ViewController.swift | 3 +- .../Containers/Views/ContainerModel.swift | 6 ++- 31 files changed, 248 insertions(+), 62 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 6812424b..8cc0bdda 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -187,7 +187,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat try container.encodeIfPresent(disabledAccessibilityTraits, forKey: .disabledAccessibilityTraits) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return title == model.title && enabled == model.enabled @@ -195,11 +195,25 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat && action.isEqual(to: model.action) && accessibilityText == model.accessibilityText && accessibilityIdentifier == model.accessibilityIdentifier + && accessibilityTraits == model.accessibilityTraits + && disabledAccessibilityTraits == model.disabledAccessibilityTraits && style == model.style && size == model.size && groupName == model.groupName && width == model.width + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return title == model.title + && enabled == model.enabled + && inverted == model.inverted + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier && accessibilityTraits == model.accessibilityTraits && disabledAccessibilityTraits == model.disabledAccessibilityTraits + && style == model.style + && size == model.size + && width == model.width } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift index 38cbe673..a8a80629 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -100,7 +100,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && title == model.title @@ -110,7 +110,18 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode && enabled == model.enabled && size == model.size && shouldMaskRecordedView == model.shouldMaskRecordedView -// && action.isEqual(to: model.action) // TODO: Move to isVisiuallyEquivalent. + && action.isEqual(to: model.action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && title == model.title + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && inverted == model.inverted + && enabled == model.enabled + && size == model.size } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 7a836146..8aa1ca48 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -21,6 +21,8 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro public var id: String = UUID().uuidString public var backgroundColor: Color? public var moleculeName: String? + + // Assigned and computed by parent. public var numberOfPages: Int = 0 /// Sets the current Index to focus on. @@ -49,7 +51,6 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro case moleculeName case backgroundColor case currentIndex - case numberOfPages case alwaysSendAction case animated case hidesForSinglePage @@ -118,7 +119,6 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro try container.encode(id, forKey: .id) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) - try container.encode(numberOfPages, forKey: .numberOfPages) try container.encode(currentIndex, forKey: .currentIndex) try container.encode(alwaysSendAction, forKey: .alwaysSendAction) try container.encode(animated, forKey: .animated) @@ -131,10 +131,9 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro try container.encodeIfPresent(position, forKey: .position) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && numberOfPages == model.numberOfPages && currentIndex == model.currentIndex && alwaysSendAction == model.alwaysSendAction && animated == model.animated diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift index e65af621..1dbac837 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift @@ -49,8 +49,12 @@ open class LabelAttributeActionModel: LabelAttributeModel { try container.encodeModel(action, forKey: .action) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return action.isEqual(to: model.action) } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + return super.isEqual(to: model) + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift index 68a59d4d..e1c4ccb1 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift @@ -44,7 +44,7 @@ try container.encodeIfPresent(textColor, forKey: .textColor) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return textColor == model.textColor } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift index 931c0d63..26cde0fc 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift @@ -56,7 +56,7 @@ try container.encodeIfPresent(size, forKey: .size) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return style == model.style && name == model.name diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift index 0dceaf7d..2c9fdcd7 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift @@ -70,7 +70,7 @@ class LabelAttributeImageModel: LabelAttributeModel { try container.encodeIfPresent(tintColor, forKey: .tintColor) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return URL == model.URL && name == model.name diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index 35ad5700..6da10fcc 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -7,7 +7,7 @@ // -@objcMembers open class LabelAttributeModel: ModelProtocol { +@objcMembers open class LabelAttributeModel: ModelProtocol, ModelComparisonProtocol, MoleculeModelComparisonProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -76,7 +76,7 @@ try container.encode(length, forKey: .length) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return location == model.location && length == model.length diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift index b2a226e5..cf616ef0 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift @@ -67,7 +67,7 @@ import UIKit try container.encodeIfPresent(pattern, forKey: .pattern) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return style == model.style && color == model.color diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index d3611a2e..dbddacef 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -133,7 +133,7 @@ import VDS try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && text == model.text @@ -143,12 +143,31 @@ import VDS && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.areEqual(to: model.attributes) + && attributes.isEqual(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable && numberOfLines == model.numberOfLines + && accessibilityTraits == model.accessibilityTraits + && inverted == inverted && shouldMaskRecordedView == model.shouldMaskRecordedView + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && text == model.text + && textColor == model.textColor + && fontStyle == model.fontStyle + && fontName == model.fontName + && fontSize == model.fontSize + && textAlignment == model.textAlignment + && attributes.isEqual(to: model.attributes) + && html == model.html + && hero == model.hero + && makeWholeViewClickable == model.makeWholeViewClickable + && numberOfLines == model.numberOfLines + && accessibilityText == model.accessibilityText && accessibilityTraits == model.accessibilityTraits && inverted == inverted } diff --git a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift index 7c3e66ec..8ac93a4f 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift @@ -130,7 +130,7 @@ public class LineModel: MoleculeModelProtocol, Invertable { try container.encode(orientation == .vertical, forKey: .useVerticalLine) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return type == model.type && inverted == model.inverted diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift index 3e4c66bf..c40331c7 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift @@ -68,9 +68,4 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod try container.encode(moleculeName, forKey: .moleculeName) try container.encode(headlineBody, forKey: .headlineBody) } - - public func isEqual(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return headlineBody.isEqual(to: headlineBody) - } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 582b3095..14b0969d 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -46,14 +46,24 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { return nil } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return textAlignment == model.textAlignment && subTitleColor == model.subTitleColor && alignment == model.alignment && inverted == model.inverted && backgroundColor == model.backgroundColor - && children.areEqual(to: model.children) + && children.isEqual(to: model.children) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return textAlignment == model.textAlignment + && subTitleColor == model.subTitleColor + && alignment == model.alignment + && inverted == model.inverted + && backgroundColor == model.backgroundColor + && children.isVisuallyEquivalent(to: model.children) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index c412b09c..f7b875c1 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -91,7 +91,7 @@ try container.encode(readOnly, forKey: .readOnly) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } return peakingUI == model.peakingUI && peakingArrowColor == model.peakingArrowColor @@ -100,4 +100,12 @@ && enabled == model.enabled && readOnly == model.readOnly } + + public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + return peakingUI == model.peakingUI + && peakingArrowColor == model.peakingArrowColor + && enabled == model.enabled + && readOnly == model.readOnly + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index b156743d..bf629f7a 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -8,7 +8,7 @@ // A base class that has common list item boilerplate model stuffs. import MVMCore -@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol { +@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol, ModelComparisonProtocol, MoleculeModelComparisonProtocol { //-------------------------------------------------- // MARK: - Properties @@ -23,6 +23,7 @@ import MVMCore public var accessibilityTraits: UIAccessibilityTraits? public var accessibilityValue: String? public var accessibilityText: String? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- @@ -129,4 +130,29 @@ import MVMCore try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && hideArrow == model.hideArrow + && style == model.style + && gone == model.gone + && accessibilityText == model.accessibilityText + && accessibilityValue == model.accessibilityValue + && accessibilityTraits == model.accessibilityTraits + && line.isEqual(to: model.line) + && action.isEqual(to: model.action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && hideArrow == model.hideArrow + && style == model.style + && gone == model.gone + && accessibilityText == model.accessibilityText + && accessibilityValue == model.accessibilityValue + && accessibilityTraits == model.accessibilityTraits + && line.isVisuallyEquivalent(to: model.line) + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 383c4692..9bfa69c0 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -74,9 +74,14 @@ try super.encode(to: encoder) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } - return action.isEqual(to: model.action) - && border == border + return border == border + && action.isEqual(to: model.action) + } + + public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + return border == border } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift index 48b4fc2a..82958a12 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift @@ -53,17 +53,13 @@ import MVMCore try container.encodeModel(molecule, forKey: .molecule) } - public func isEqual(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && action.isEqual(to: model.action) - && hideArrow == model.hideArrow - && line.isEqual(to: model.line) - && style == model.style - && gone == model.gone - && molecule.isEqual(to: model.molecule) - && accessibilityTraits == model.accessibilityTraits - && accessibilityValue == model.accessibilityValue - && accessibilityText == model.accessibilityText + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return molecule.isEqual(to: model) + } + + public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + return molecule.isVisuallyEquivalent(to: model.molecule) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift index 75dcfd4f..72b9ee24 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift @@ -59,10 +59,16 @@ try container.encode(gone, forKey: .gone) } - public override func isEqual(to model: any ModelProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && spacing == model.spacing + return spacing == model.spacing + && percent == model.percent + && gone == model.gone + } + + public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + return spacing == model.spacing && percent == model.percent && gone == model.gone } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 19e70358..731cfccd 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -30,7 +30,10 @@ import UIKit } public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - MoleculeContainer.nameForReuse(with: model, delegateObject) + if let listModel = model as? ListItemModel, listModel.hasStableId { + return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" + } + return MoleculeContainer.nameForReuse(with: model, delegateObject) } public override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { diff --git a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift index bd496f3a..85c4cf86 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift @@ -37,7 +37,7 @@ fatalError("init(from:) has not been implemented") } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && spacing == model.spacing diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index ce271ba6..a31f801b 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -62,9 +62,14 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return molecule.isEqual(to: model.molecule) && backgroundColor == model.backgroundColor } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index df7b897e..bc9dad7f 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -107,9 +107,9 @@ public class EyebrowHeadlineBodyLinkModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(link, forKey: .link) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && children.areEqual(to: model.children) + && children.isEqual(to: model.children) } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index a062677b..f556d675 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -94,7 +94,7 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return headline.isEqual(to: model.headline) && body.isEqual(to: model.body) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 914e6427..fdd7f5b6 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -166,9 +166,19 @@ open class Carousel: View { public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { self.delegateObject = delegateObject + let originalModel = self.model as? CarouselModel super.set(with: model, delegateObject, additionalData) guard let carouselModel = model as? CarouselModel else { return } + + if #available(iOS 15.0, *) { + if let originalModel, carouselModel.isVisuallyEquivalent(to: originalModel) { + collectionView.reconfigureItems(at: collectionView.indexPathsForVisibleItems) + FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate) + return + } + } + accessibilityLabel = carouselModel.accessibilityText collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0 diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 4e645a92..449d2abf 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -172,10 +172,10 @@ import UIKit try container.encode(readOnly, forKey: .readOnly) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == backgroundColor - && molecules.areEqual(to: model.molecules) + && molecules.isEqual(to: model.molecules) && spacing == model.spacing && border == model.border && loop == model.loop @@ -194,6 +194,27 @@ import UIKit && enabled == model.enabled && readOnly == model.readOnly } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == backgroundColor + && spacing == model.spacing + && border == model.border + && loop == model.loop + && height == model.height + && itemWidthPercent == model.itemWidthPercent + && itemAlignment == model.itemAlignment + && paging == model.paging + && useHorizontalMargins == model.useHorizontalMargins + && leftPadding == model.leftPadding + && rightPadding == model.rightPadding + && accessibilityText == model.accessibilityText + && baseValue == model.baseValue + && enabled == model.enabled + && readOnly == model.readOnly + && pagingMolecule.isVisuallyEquivalent(to: model.pagingMolecule) + && molecules.isVisuallyEquivalent(to: model.molecules) + } } extension CarouselModel { diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 7f03df14..087717c1 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -79,10 +79,10 @@ try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - public func isEqual(to model: any ModelProtocol) -> Bool { + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && molecules.areEqual(to: model.molecules) + && molecules.isEqual(to: model.molecules) && axis == model.axis && spacing == model.spacing } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 907c2cfa..5465c8e4 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -8,11 +8,12 @@ import Foundation -public protocol MoleculeModelComparisonProtocol: ModelProtocol { +public protocol MoleculeModelComparisonProtocol: ModelComparisonProtocol { - /** True if there are no visual differences between models. + /** + True if there are no visual differences between models. - By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. + By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. **/ func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol) -> Bool } @@ -23,3 +24,39 @@ extension MoleculeModelComparisonProtocol { return isEqual(to: model) } } + +public extension Optional { + + /// Checks if the curent model is equal to another model. + func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol?) -> Bool { + guard let self = self as? MoleculeModelComparisonProtocol else { + return model == nil + } + guard let model = model else { + return false + } + return self.isVisuallyEquivalent(to: model) + } +} + +public extension Collection { + /// Checks if all the models in the given collection match another given collection. + func isVisuallyEquivalent(to models: [MoleculeModelComparisonProtocol]) -> Bool { + guard count == models.count, let self = self as? [MoleculeModelComparisonProtocol] else { return false } + return models.indices.allSatisfy { index in + self[index].isVisuallyEquivalent(to: models[index]) + } + } +} + +public extension Optional where Wrapped: Collection { + func isVisuallyEquivalent(to models: [MoleculeModelComparisonProtocol]?) -> Bool { + guard let self = self as? [MoleculeModelComparisonProtocol] else { + return models == nil + } + guard let models = models else { + return false + } + return self.isVisuallyEquivalent(to: models) + } +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index c355d3f6..bf55de18 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -19,7 +19,7 @@ public extension MoleculeModelProtocol { static var categoryCodingKey: String { "moleculeName" } - func isEqual(to model: any ModelProtocol) -> Bool { + func isEqual(to model: ModelProtocol) -> Bool { guard let model = model as? Self else { return false } return id == model.id } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 88817e6a..c2f1e886 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -69,7 +69,12 @@ public extension ParentMoleculeModelProtocol { func isEqual(to model: any ModelProtocol) -> Bool { guard let model = model as? Self else { return false } - return model.children.areEqual(to: model.children) + return model.children.isEqual(to: model.children) + } + + func areVisuallyEquivalent(to model: any ModelProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.children.isVisuallyEquivalent(to: model.children) } func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int) -> Result) -> Result { @@ -119,7 +124,14 @@ public extension ParentMoleculeModelProtocol { extension ParentModelProtocol { - func deepCompare(_ anotherParent: ParentModelProtocol, with test: (ModelProtocol, ModelProtocol)->Bool) -> (Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?) { + public typealias DeepCompareResult = (matched: Bool, myChild: ModelProtocol?, theirChild: 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) } + } + + func deepCompare(_ anotherParent: ParentModelProtocol, with test: (ModelProtocol, ModelProtocol)->Bool) -> DeepCompareResult { guard test(self, anotherParent) else { return (false, myChild: self, theirChild: self)} diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index c146afa0..fe13c3eb 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -522,7 +522,8 @@ import MVMCore return (model, replacedMolecule) } let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in - guard !new.isVisuallyEquivalent(to: existing) else { + guard !new.isEqual(to: existing) else { + MVMCoreLoggingHandler.shared()?.handleDebugMessage("UI for molecules: \(new) is the same. Skip UI update.") return nil } return new diff --git a/MVMCoreUI/Containers/Views/ContainerModel.swift b/MVMCoreUI/Containers/Views/ContainerModel.swift index b54590dd..81b7c769 100644 --- a/MVMCoreUI/Containers/Views/ContainerModel.swift +++ b/MVMCoreUI/Containers/Views/ContainerModel.swift @@ -14,6 +14,7 @@ open class ContainerModel: ContainerModelProtocol, Codable { //-------------------------------------------------- public var id: String = UUID().uuidString + public var hasStableId = false public var horizontalAlignment: UIStackView.Alignment? public var useHorizontalMargins: Bool? @@ -78,7 +79,10 @@ open class ContainerModel: ContainerModelProtocol, Codable { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let id = try typeContainer.decodeIfPresent(String.self, forKey: .id) { + self.id = id + hasStableId = true + } if let verticalAlignmentString = try typeContainer.decodeIfPresent(String.self, forKey: .verticalAlignment) { verticalAlignment = ContainerHelper.getAlignment(for: verticalAlignmentString) From 9de1437edc2e256b8af61f9b4898e5df7f708b1e Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 8 May 2024 20:36:17 -0400 Subject: [PATCH 24/64] Digital PCT265 story ONEAPP-7249 - Prevent SplitViewController forced layouts on every newDataBuildScreen update. --- .../SplitViewController/MVMCoreUISplitViewController.m | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 453c7d56..4d865830 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -839,12 +839,11 @@ CGFloat const PanelAnimationDuration = 0.2; - (void)setBottomProgressBarProgress:(float)progress { [MVMCoreDispatchUtility performBlockOnMainThread:^{ - if (self.bottomProgressBarHeightConstraint.constant != PaddingOne) { - self.bottomProgressBarHeightConstraint.constant = PaddingOne; - [self.bottomProgressBar.superview layoutIfNeeded]; - } - if (progress > 0.05) { + if (self.bottomProgressBarHeightConstraint.constant != PaddingOne) { + self.bottomProgressBarHeightConstraint.constant = PaddingOne; + [self.bottomProgressBar.superview layoutIfNeeded]; + } self.bottomProgressBar.progress = progress; } else { self.bottomProgressBarHeightConstraint.constant = 0; From f37e7abcb176f94a3050c2a0c2f326fd2026681a Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Mon, 13 May 2024 14:46:35 -0400 Subject: [PATCH 25/64] Digital PCT265 story PCT-135: Inline replacement updates with the core render loop. --- .../Accessibility/AccessibilityHandler.swift | 3 +- .../CarouselIndicatorModel.swift | 12 ++ .../Atomic/Organisms/Carousel/Carousel.swift | 8 +- .../Organisms/Carousel/CarouselModel.swift | 8 + .../ParentMoleculeModelProtocol.swift | 32 +++- .../TemplateModelProtocol.swift | 2 + .../Atomic/Protocols/TemplateProtocol.swift | 23 ++- .../Atomic/Templates/BaseTemplateModel.swift | 3 + .../Atomic/Templates/CollectionTemplate.swift | 9 +- .../Templates/ModalMoleculeListTemplate.swift | 4 +- .../ModalMoleculeStackTemplate.swift | 4 +- .../Templates/MoleculeListTemplate.swift | 10 +- .../Templates/MoleculeStackTemplate.swift | 9 +- .../Atomic/Templates/ThreeLayerTemplate.swift | 5 +- .../BaseControllers/ViewController.swift | 156 +++++++++++++----- .../Behaviors/PollingBehaviorModel.swift | 5 +- .../Protocols/PageBehaviorProtocol.swift | 8 +- .../ReplaceableMoleculeBehaviorModel.swift | 117 ++++++------- 18 files changed, 266 insertions(+), 152 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 65ae8272..76e556e6 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -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?) { diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 8aa1ca48..d793dc98 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -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 + } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index fdd7f5b6..1db78373 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -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 diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 449d2abf..1ef5b98f 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -228,3 +228,11 @@ extension CarouselModel { } } + +extension CarouselModel: CustomDebugStringConvertible { + + public var debugDescription: String { + return "\(molecules.count) \(molecules.map { ($0 as? CarouselItemModel)?.molecule.moleculeName ?? "unknown" } )" + } + +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index c2f1e886..25dbcdc7 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -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 + } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 5507eadb..2ee8a04a 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -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 { diff --git a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift index 41c8f56a..44584305 100644 --- a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift @@ -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 } } diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index 8700fed1..78c7d61b 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -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. diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index 100b1b17..e9e8b46f 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -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) { diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift index 8f8ca005..16afd5bd 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift @@ -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 } diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift index 97aced2a..f3264b1b 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift @@ -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 ?? diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index d62fd149..ad908568 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -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 } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift index 8ef02f39..8ce8cd67 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift @@ -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? { diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift index b0e36239..b4af2ed4 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift @@ -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) { diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index fe13c3eb..0486f6b4 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -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) -> 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) -> 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" + } + +} diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index 0d40f4cd..719e22ea 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -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?) { diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index ae09e0ce..6cc30e95 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift @@ -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) {} diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 08f9c2f9..4febdcc1 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -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() } } From 32deda3d3d7918d1c658e43f3ce3a309e189fd3a Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Mon, 13 May 2024 21:23:00 -0400 Subject: [PATCH 26/64] Digital PCT265 story PCT-135: Code review comments, cleanups and isEquals expansion. --- .../Atomic/Actions/ActionAlertModel.swift | 7 +++ .../ActionCollapseNotificationModel.swift | 8 +++ .../ActionDismissNotificationModel.swift | 8 +++ .../Atomic/Actions/ActionOpenPanelModel.swift | 8 +++ .../Actions/ActionTopNotificationModel.swift | 7 +++ MVMCoreUI/Atomic/Actions/AlertModel.swift | 27 +++++++-- .../Items/MoleculeTableViewCell.swift | 6 +- .../Protocols/MoleculeDelegateProtocol.swift | 3 - .../BaseControllers/ViewController.swift | 56 +++---------------- .../Behaviors/AddRemoveMoleculeBehavior.swift | 21 +++++++ MVMCoreUI/Behaviors/GetContactBehavior.swift | 6 ++ .../GetNotificationAuthStatusBehavior.swift | 6 ++ .../Behaviors/PollingBehaviorModel.swift | 9 +++ .../PageBehaviorHandlerProtocol.swift | 1 + .../Protocols/PageBehaviorModelProtocol.swift | 5 ++ .../ReplaceableMoleculeBehaviorModel.swift | 11 +++- .../ScreenBrightnessModifierBehavior.swift | 10 +++- .../Rules/Rules/RuleEqualsModel.swift | 2 +- .../Notification/NotificationModel.swift | 12 +++- 19 files changed, 149 insertions(+), 64 deletions(-) diff --git a/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift b/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift index f657304b..1787da20 100644 --- a/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift @@ -25,4 +25,11 @@ public struct ActionAlertModel: ActionModelProtocol { public init(alert: AlertModel) { self.alert = alert } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.alert == alert + } } diff --git a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift index 94e84744..71ed49af 100644 --- a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift @@ -20,4 +20,12 @@ public struct ActionCollapseNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters self.analyticsData = analyticsData } + + // Default + //public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + // guard let model = model as? Self else { return false } + // return model.actionType == actionType + // && model.extraParameters == extraParameters + // && model.analyticsData == analyticsData + //} } diff --git a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift index 13816f37..e5e2781a 100644 --- a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift @@ -20,4 +20,12 @@ public struct ActionDismissNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters self.analyticsData = analyticsData } + + // Default + // public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + // guard let model = model as? Self else { return false } + // return model.actionType == actionType + // && model.extraParameters == extraParameters + // && model.analyticsData == analyticsData + //} } diff --git a/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift b/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift index ec526e10..2a69eb05 100644 --- a/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift @@ -29,4 +29,12 @@ public struct ActionOpenPanelModel: ActionModelProtocol { self.extraParameters = extraParameters self.analyticsData = analyticsData } + + // Default + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.panel == panel + } } diff --git a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift index 07d13230..b5e0584f 100644 --- a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift @@ -22,4 +22,11 @@ public struct ActionTopNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters self.analyticsData = analyticsData } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.topNotification == topNotification + } } diff --git a/MVMCoreUI/Atomic/Actions/AlertModel.swift b/MVMCoreUI/Atomic/Actions/AlertModel.swift index 8a36ab03..683d87a4 100644 --- a/MVMCoreUI/Atomic/Actions/AlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/AlertModel.swift @@ -9,7 +9,8 @@ import UIKit import MVMCore -public struct AlertButtonModel: Codable { +public struct AlertButtonModel: Codable, Equatable { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -62,14 +63,21 @@ public struct AlertButtonModel: Codable { try container.encodeModel(action, forKey: .action) try container.encodeIfPresent(preferred, forKey: .preferred) } + + public static func == (lhs: AlertButtonModel, rhs: AlertButtonModel) -> Bool { + lhs.title == rhs.title + && lhs.preferred == rhs.preferred + && lhs.style == rhs.style + && lhs.action.isEqual(to: rhs.action) + } } -public struct AlertModel: Codable, Identifiable, AlertModelProtocol { +public struct AlertModel: Codable, Identifiable, Equatable, AlertModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - + public var title: String? public var message: String? public var preferredStyle: UIAlertController.Style = .alert @@ -78,7 +86,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { public var id: String public var delegateObject: DelegateObject? - + public var actions: [UIAlertAction] { get { buttonModels.map({ alertButtonModel in @@ -94,8 +102,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { }) } } - - + //-------------------------------------------------- // MARK: - Init //-------------------------------------------------- @@ -149,6 +156,14 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encode(id, forKey: .id) } + + public static func == (lhs: AlertModel, rhs: AlertModel) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.title + && lhs.preferredStyle == rhs.preferredStyle + && lhs.buttonModels == rhs.buttonModels + && lhs.analyticsData == rhs.analyticsData + } } public extension AlertButtonModel { diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 731cfccd..01be79bd 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -30,9 +30,9 @@ import UIKit } public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - if let listModel = model as? ListItemModel, listModel.hasStableId { - return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" - } +// if let listModel = model as? ListItemModel, listModel.hasStableId { +// return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" +// } return MoleculeContainer.nameForReuse(with: model, delegateObject) } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift index e8cae35b..e78c0b1b 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift @@ -21,9 +21,6 @@ public protocol MoleculeDelegateProtocol: AnyObject { /// Notifies the delegate that the molecule layout update. Should be called when the layout may change due to an async method. Mainly used for list or collections. func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) //optional - - /// Attempts to replace the molecules provided. Returns the ones that replaced successfully. - func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)?) } extension MoleculeDelegateProtocol { diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 0486f6b4..4fd8d5f5 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -150,7 +150,7 @@ 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. - isFirstRender = true + 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)) } @@ -234,7 +234,7 @@ import MVMCore open func handleNewData(_ pageModel: PageModelProtocol? = nil) { guard var newPageModel = pageModel ?? self.pageModel else { return } - let originalModel = self.isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil + let originalModel = isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil if originalModel != nil, let behaviorContainer = newPageModel as? PageBehaviorContainerModelProtocol { var behaviorHandler = self @@ -246,8 +246,10 @@ import MVMCore newPageModel.navigationBar = navigationItem } + // Make the template available for onPageNew behavior handling. See if we can have behaviors rely on roots later. self.pageModel = newPageModel + // Run through behavior tranformations. var behaviorUpdatedModels = [MoleculeModelProtocol]() if var newTemplateModel = newPageModel as? TemplateModelProtocol { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in @@ -258,6 +260,8 @@ import MVMCore 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.) } + } else { + debugLog("Failed to replace \(molecule) in the template model.") } } } @@ -265,13 +269,14 @@ import MVMCore } if formValidator == nil { // TODO: Can't change form rules? - let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules + let rules = (newPageModel as? FormHolderModelProtocol)?.formRules formValidator = FormValidator(rules) } + // Reset after tranformations. self.pageModel = newPageModel - /// Run through the differences between separate page model trees. + // Run through the differences between separate page model trees. var pageUpdatedModels = [MoleculeModelProtocol]() if let originalModel, // We had a prior. let newPageModel = newPageModel as? TemplateModelProtocol, @@ -573,49 +578,6 @@ import MVMCore // Needed otherwise when subclassed, the extension gets called. open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } - public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) { - pageUpdateQueue.addOperation { [self] in - let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in - 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 { - debugLog("UI for molecules: \(new) is the same. Skip UI update.") - return nil - } - return new - } - if uiUpdatedModels.count > 0 { - debugLog("Updating UI for molecules: \(uiUpdatedModels)") - DispatchQueue.main.sync { - self.updateUI(for: uiUpdatedModels) - } - } - completionHandler?(replacedModels.map { $0.0 }) - } - } - - open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> MoleculeModelProtocol? { - guard var templateModel = getTemplateModel() else { return nil } - var replacedMolecule: MoleculeModelProtocol? - do { - replacedMolecule = try templateModel.replaceMolecule(with: replacementModel) - if replacedMolecule == nil { - MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(replacementModel.id)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!) - } - } catch { - let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! - if let error = error as? HumanReadableDecodingErrorProtocol { - coreError.messageToLog = "Error replacing molecule \"\(replacementModel.id)\": \(error.readableDescription)" - } - MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) - } - return replacedMolecule - } - //-------------------------------------------------- // MARK: - MVMCoreUIDetailViewProtocol //-------------------------------------------------- diff --git a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift index 3ea65249..0288ee1b 100644 --- a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift +++ b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift @@ -159,6 +159,13 @@ public class AddMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.animation == animation + } } public class RemoveMoleculesActionModel: ActionModelProtocol { @@ -186,6 +193,13 @@ public class RemoveMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.animation == animation + } } public class SwapMoleculesActionModel: ActionModelProtocol { @@ -213,4 +227,11 @@ public class SwapMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return model.extraParameters == extraParameters + && model.analyticsData == analyticsData + && model.animation == animation + } } diff --git a/MVMCoreUI/Behaviors/GetContactBehavior.swift b/MVMCoreUI/Behaviors/GetContactBehavior.swift index 755c5706..b0281fd8 100644 --- a/MVMCoreUI/Behaviors/GetContactBehavior.swift +++ b/MVMCoreUI/Behaviors/GetContactBehavior.swift @@ -20,6 +20,12 @@ public class PageGetContactBehaviorModel: PageBehaviorModelProtocol { public var shouldAllowMultipleInstances: Bool { false } public init() { } + + // Default + // public func isEqual(to model: any MVMCore.ModelComparisonProtocol) -> Bool { + // guard let model = model as? Self else { return false } + // return behaviorName == model.behaviorName + //} } public class PageGetContactBehavior: PageVisibilityBehavior { diff --git a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift index 980f0dbf..4cbb05b4 100644 --- a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift +++ b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift @@ -15,6 +15,12 @@ public class GetNotificationAuthStatusBehaviorModel: PageBehaviorModelProtocol { public var shouldAllowMultipleInstances: Bool { false } public init() { } + + // Default + // public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + // guard let model = model as? Self else { return false } + // return behaviorName == model.behaviorName + //} } public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior { diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index 719e22ea..5f87593b 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -46,6 +46,15 @@ public class PollingBehaviorModel: PageBehaviorModelProtocol { try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad) try container.encode(refreshOnShown, forKey: .refreshOnShown) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return runWhileHidden == model.runWhileHidden + && refreshOnShown == model.refreshOnShown + && refreshOnFirstLoad == model.refreshOnFirstLoad + && refreshInterval == model.refreshInterval + && refreshAction.isEqual(to: model.refreshAction) + } } extension PollingBehaviorModel: CustomDebugStringConvertible { diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift index 5c6a78bb..566a3f3d 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift @@ -33,6 +33,7 @@ public extension PageBehaviorHandlerProtocol { mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { // Pull the existing behaviors. var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates } + // Create and append any new behaviors based on the incoming models. let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject) behaviors.append(contentsOf: newBehaviors) diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift index 8b9eabab..510533d2 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift @@ -33,4 +33,9 @@ public extension PageBehaviorModelProtocol { static var categoryName: String { "\(PageBehaviorModelProtocol.self)" } + + func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return behaviorName == model.behaviorName + } } diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 4febdcc1..66712434 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -13,6 +13,11 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol { public class var identifier: String { "replaceMoleculeBehavior" } public var shouldAllowMultipleInstances: Bool { true } public var moleculeIds: [String] + + public func isEqual(to model: any MVMCore.ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeIds == model.moleculeIds + } } public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging { @@ -92,7 +97,11 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co hasReplacement = true } } catch { - + let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! + if let error = error as? HumanReadableDecodingErrorProtocol { + coreError.messageToLog = "Error replacing molecule \"\(newMolecule.id)\": \(error.readableDescription)" + } + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) } } return parentMolecule diff --git a/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift b/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift index 59d80211..e830172a 100644 --- a/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift +++ b/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift @@ -7,13 +7,14 @@ // public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol { + public var shouldAllowMultipleInstances: Bool = false public static var identifier = "screenBrightnessModifier" @Clamping(range: 0...1) var screenBrightness: CGFloat var originalScreenBrightness: CGFloat? //MARK:- Codable - + private enum CodingKeys: String, CodingKey { case screenBrightness } @@ -22,11 +23,16 @@ public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) screenBrightness = try typeContainer.decode(CGFloat.self, forKey: .screenBrightness) } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(screenBrightness, forKey: .screenBrightness) } + + public func isEqual(to model: ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return screenBrightness == model.screenBrightness + } } public class ScreenBrightnessModifierBehavior: PageVisibilityBehavior { diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift index be7391ec..73958d74 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift @@ -15,7 +15,7 @@ public class RuleEqualsModel: RuleCompareModelProtocol { public static var identifier: String = "equals" public var type: String = RuleEqualsModel.identifier - public var ruleId: String? + public var ruleId: String? public var fields: [String] public var errorMessage: [String: String]? diff --git a/MVMCoreUI/Notification/NotificationModel.swift b/MVMCoreUI/Notification/NotificationModel.swift index 435d95b6..70736064 100644 --- a/MVMCoreUI/Notification/NotificationModel.swift +++ b/MVMCoreUI/Notification/NotificationModel.swift @@ -9,7 +9,7 @@ import Foundation import MVMCore -open class NotificationModel: Codable, Identifiable { +open class NotificationModel: Codable, Identifiable, Equatable { public var type: String public var priority = Operation.QueuePriority.normal public var molecule: MoleculeModelProtocol @@ -115,4 +115,14 @@ open class NotificationModel: Codable, Identifiable { try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encode(id, forKey: .id) } + + public static func == (lhs: NotificationModel, rhs: NotificationModel) -> Bool { + lhs.persistent == rhs.persistent + && lhs.priority == rhs.priority + && lhs.type == rhs.type + && lhs.persistent == rhs.persistent + && lhs.dismissTime == rhs.dismissTime + && lhs.pages == rhs.pages + && lhs.analyticsData == rhs.analyticsData + } } From cd5d9b0c4aed071be94772b778191f3a6da5c99c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 14 May 2024 20:30:11 -0400 Subject: [PATCH 27/64] Digital PCT265 story PCT-135: Pager fix. TwoButtonViewModel isEqual. --- .../TwoButtonViewModel.swift | 16 ++++++++++++++++ .../Atomic/Organisms/Carousel/Carousel.swift | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index dc471e0a..2d978f17 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -86,4 +86,20 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(secondaryButton, forKey: .secondaryButton) try container.encodeIfPresent(fillContainer, forKey: .fillContainer) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && fillContainer == model.fillContainer + && primaryButton.isEqual(to: model.primaryButton) + && secondaryButton.isEqual(to: model.secondaryButton) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && fillContainer == model.fillContainer + && primaryButton.isVisuallyEquivalent(to: model.primaryButton) + && secondaryButton.isVisuallyEquivalent(to: model.secondaryButton) + } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 1db78373..acd44595 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -178,7 +178,8 @@ open class Carousel: View { // 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. + updateModelIndex() // Ensure the new model indexing matches the old. + pagingView?.currentIndex = pageIndex // Trigger a paging view render. collectionView.reconfigureItems(at: collectionView.indexPathsForVisibleItems) return } From ffc36c309fd9cd2f6c9f8a639c276b48b95b7633 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 15 May 2024 22:24:03 -0400 Subject: [PATCH 28/64] Digital PCT265 story PCT-135: More isEquals. Fix replaceChildMolecule signature for TabsListItemModel. --- .../Atomic/Atoms/Views/Label/LabelModel.swift | 2 +- .../TabBarModel.swift | 35 ++++++++++++++- .../TabsModel.swift | 44 +++++++++++++++++-- .../Molecules/Items/TabsListItemModel.swift | 23 ++++++++-- .../NavigationBar/NavigationItemModel.swift | 11 +++++ .../HeadlineBodyModel.swift | 8 ++++ MVMCoreUI/Atomic/Organisms/StackModel.swift | 8 ++++ .../MoleculeTreeTraversalProtocol.swift | 2 +- .../ReplaceableMoleculeBehaviorModel.swift | 11 ++--- 9 files changed, 129 insertions(+), 15 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index dbddacef..e3d37cf4 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -162,7 +162,7 @@ import VDS && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.isEqual(to: model.attributes) + && attributes.isVisuallyEquivalent(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift index 9a700903..03bd6dc8 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -106,9 +106,28 @@ open class TabBarModel: MoleculeModelProtocol { try container.encode(selectedTab, forKey: .selectedTab) try container.encodeIfPresent(style, forKey: .style) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && selectedColor == model.selectedColor + && selectedTab == model.selectedTab + && style == model.style + && tabs == model.tabs + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && selectedColor == model.selectedColor + && selectedTab == model.selectedTab + && style == model.style + && tabs.isVisuallyEquivalent(to: model.tabs) + } } -open class TabBarItemModel: Codable { +open class TabBarItemModel: Codable, Equatable, MoleculeModelComparisonProtocol { + open var title: String? open var image: String open var action: ActionModelProtocol @@ -142,4 +161,18 @@ open class TabBarItemModel: Codable { try container.encodeModel(action, forKey: .action) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } + + public static func == (lhs: TabBarItemModel, rhs: TabBarItemModel) -> Bool { + return lhs.title == rhs.title + && lhs.image == rhs.image + && lhs.accessibilityText == rhs.accessibilityText + && lhs.action.isEqual(to: rhs.action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return image == model.image + && accessibilityText == model.accessibilityText + && title == model.title + } } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index a7acadf6..9f9ef766 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -105,11 +105,39 @@ open class TabsModel: MoleculeModelProtocol { try container.encode(borderLine, forKey: .borderLine) try container.encodeIfPresent(minWidth, forKey: .minWidth) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && style == model.style + && orientation == model.orientation + && indicatorPosition == model.indicatorPosition + && overflow == model.overflow + && fillContainer == model.fillContainer + && size == model.size + && borderLine == model.borderLine + && minWidth == model.minWidth + && selectedIndex == model.selectedIndex + && tabs.isEqual(to: model.tabs) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && style == model.style + && orientation == model.orientation + && indicatorPosition == model.indicatorPosition + && overflow == model.overflow + && fillContainer == model.fillContainer + && size == model.size + && borderLine == model.borderLine + && minWidth == model.minWidth + //&& selectedIndex == model.selectedIndex // Selected index could have been either reset locally or by server. For now ignore.c + && tabs.isVisuallyEquivalent(to: model.tabs) + } } - - -open class TabItemModel: Codable { +open class TabItemModel: Codable, Equatable, MoleculeModelComparisonProtocol { open var label: LabelModel open var action: ActionModelProtocol? @@ -146,4 +174,14 @@ open class TabItemModel: Codable { try container.encodeModel(label, forKey: .label) try container.encodeModelIfPresent(action, forKey: .action) } + + public static func == (lhs: TabItemModel, rhs: TabItemModel) -> Bool { + return lhs.label.isEqual(to: rhs.label) + && lhs.action.isEqual(to: rhs.action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return label.isVisuallyEquivalent(to: model.label) + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index 1924820b..a769eb89 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -23,17 +23,18 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { return molecules.flatMap { $0 } } - public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { - guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return false } + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return nil } for (tabIndex, _) in molecules.enumerated() { for (elementIndex, _) in molecules[tabIndex].enumerated() { if molecules[tabIndex][elementIndex].id == replacementMolecule.id { + let replacedMolecule = molecules[tabIndex][elementIndex] molecules[tabIndex][elementIndex] = replacementMolecule - return true + return replacedMolecule } } } - return false + return nil } //-------------------------------------------------- @@ -90,6 +91,20 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { try container.encode(tabs, forKey: .tabs) try container.encodeModels2D(molecules, forKey: .molecules) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return tabs.isEqual(to: model.tabs) + && molecules.count == model.molecules.count + && zip(molecules, model.molecules).allSatisfy({ $0.0.isEqual(to: $0.1) }) + } + + public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + return tabs.isVisuallyEquivalent(to: model.tabs) + && molecules.count == model.molecules.count + && zip(molecules, model.molecules).allSatisfy({ $0.0.isVisuallyEquivalent(to: $0.1) }) + } } extension TabsListItemModel: PageBehaviorProtocolRequirer { diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 67fc779f..f4839612 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -141,6 +141,17 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc try container.encodeIfPresent(style, forKey: .style) try container.encodeIfPresent(titleOffset, forKey: .titleOffset) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && title == model.title + && hidden == model.hidden + && tintColor == model.tintColor + && line.isEqual(to: model.line) + && hidesSystemBackButton == model.hidesSystemBackButton + && style == model.style + } } extension NavigationItemModel: ParentMoleculeModelProtocol { diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index f556d675..1c2e03a7 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -101,6 +101,14 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { && style == style && backgroundColor == backgroundColor } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return headline.isVisuallyEquivalent(to: model.headline) + && body.isVisuallyEquivalent(to: model.body) + && style == style + && backgroundColor == backgroundColor + } } public extension HeadlineBodyModel { diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 087717c1..65baa962 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -86,4 +86,12 @@ && axis == model.axis && spacing == model.spacing } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && molecules.isVisuallyEquivalent(to: model.molecules) + && axis == model.axis + && spacing == model.spacing + } } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift index b030d92a..f8e95f7b 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift @@ -38,7 +38,7 @@ public extension MoleculeTreeTraversalProtocol { func printMolecules(options: TreeTraversalOptions = .parentFirst) { depthFirstTraverse(options: options, depth: 1) { depth, molecule, stop in - print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule): \(molecule.id)]") + print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule)]") } } diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 66712434..698162f6 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -65,17 +65,17 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co } fileprivate func findAndReplace(_ moleculeModels: [any MoleculeModelProtocol], in rootMolecules: [any MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? { - debugLog("onPageNew replacing \(moleculeModels.map { $0.id })") + debugLog("attempting to replace \(moleculeModels.map { $0.id }) in \(rootMolecules)") 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...") + debugLog("molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...") return rootMolecule } - debugLog("onPageNew replacing \(rootMolecule) with \(updatedMolecule)") + debugLog("replacing \(rootMolecule) with \(updatedMolecule)") logUpdated(molecule: updatedMolecule) hasReplacement = true return updatedMolecule @@ -89,10 +89,10 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co 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...") + debugLog("molecule \(newMolecule) is the same as \(replacedMolecule). skipping...") return } - debugLog("onPageNew replacing \(replacedMolecule) with \(newMolecule)") + debugLog("replacing \(replacedMolecule) with \(newMolecule)") logUpdated(molecule: newMolecule) hasReplacement = true } @@ -106,6 +106,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co } return parentMolecule } + debugLog("replacing \(hasReplacement ? updatedRootMolecules.count : 0) molecules") return hasReplacement ? updatedRootMolecules : nil } From eac64ec5067ef42db1d6ec29a400e53db33ea3a5 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 15 May 2024 22:25:44 -0400 Subject: [PATCH 29/64] Digital PCT265 story PCT-135: handleNewData logic fix for orginalModel comparison. --- MVMCoreUI/BaseControllers/ViewController.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 4fd8d5f5..56b58b89 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -234,13 +234,16 @@ import MVMCore open func handleNewData(_ pageModel: PageModelProtocol? = nil) { guard var newPageModel = pageModel ?? self.pageModel else { return } - let originalModel = isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil + let originalModel = isFirstRender ? nil : self.pageModel as? MVMControllerModelProtocol - if originalModel != nil, let behaviorContainer = newPageModel as? PageBehaviorContainerModelProtocol { + // Refresh our behaviors if there is a page change. + if let behaviorContainer = newPageModel as? (PageBehaviorContainerModelProtocol & TemplateModelProtocol), + (originalModel == nil || originalModel!.id != behaviorContainer.id) { var behaviorHandler = self behaviorHandler.applyBehaviors(pageBehaviorModel: behaviorContainer) } + // Setup the default navigation bar if it is missing. if newPageModel.navigationBar == nil { let navigationItem = createDefaultLegacyNavigationModel() newPageModel.navigationBar = navigationItem @@ -268,6 +271,7 @@ import MVMCore } } + // Apply the form validator to the controller. if formValidator == nil { // TODO: Can't change form rules? let rules = (newPageModel as? FormHolderModelProtocol)?.formRules formValidator = FormValidator(rules) @@ -311,12 +315,13 @@ import MVMCore /// Applies the latest model to the UI. open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { - guard molecules == nil else { return } executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in - behavior.willRender(rootMolecules: getRootMolecules(), delegateObjectIVar) + behavior.willRender(rootMolecules: molecules ?? getRootMolecules(), delegateObjectIVar) } + guard molecules == nil else { return } + if let backgroundColor = model?.backgroundColor { view.backgroundColor = backgroundColor.uiColor } From 52e8ce7621f5a46c63b3832b0ca6da056cc33a90 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Thu, 16 May 2024 10:20:31 -0400 Subject: [PATCH 30/64] Digital PCT265 story PCT-135: Prevent registering table cells on ListViewTemplate handleNewData when the page is not changing. --- .../Atomic/Templates/MoleculeListTemplate.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index ad908568..ef382c0f 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -19,7 +19,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol public var moleculesInfo: [MoleculeInfo]? var observer: NSKeyValueObservation? - + //-------------------------------------------------- // MARK: - Computed Properties //-------------------------------------------------- @@ -87,8 +87,11 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func handleNewData(_ pageModel: PageModelProtocol? = nil) { super.handleNewData(pageModel) - setup() - registerWithTable() + + if pageModel != nil { + setup() + registerWithTable() + } } open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { @@ -368,7 +371,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { let removeIndex = indexPath.row - index moleculesInfo?.remove(at: removeIndex) } - + guard let animation = animation, indexPaths.count > 0 else { return } tableView?.deleteRows(at: indexPaths, with: animation) @@ -378,7 +381,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) { var indexPaths: [IndexPath] = [] - + for molecule in molecules { if let info = self.createMoleculeInfo(with: molecule) { self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier) @@ -387,7 +390,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { indexPaths.append(IndexPath(row: index, section: 0)) } } - + guard let animation = animation, indexPaths.count > 0 else { return } self.tableView?.insertRows(at: indexPaths, with: animation) From 7557e913a21b8760271705551ed6aebec7d67a1b Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 17 May 2024 14:42:35 -0400 Subject: [PATCH 31/64] Digital PCT265 defect CXTDT-546581: Tabs page state tracking. --- .../Molecules/HorizontalCombinationViews/TabsModel.swift | 6 +++++- MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index a7acadf6..16416933 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -112,6 +112,7 @@ open class TabsModel: MoleculeModelProtocol { open class TabItemModel: Codable { open var label: LabelModel open var action: ActionModelProtocol? + public var analyticsData: JSONValueDictionary? public init(label: LabelModel) { self.label = label @@ -120,6 +121,7 @@ open class TabItemModel: Codable { private enum CodingKeys: String, CodingKey { case label case action + case analyticsData } open func setDefaults() { @@ -138,12 +140,14 @@ open class TabItemModel: Codable { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) label = try typeContainer.decode(LabelModel.self, forKey: .label) action = try typeContainer.decodeModelIfPresent(codingKey: .action) + analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) setDefaults() } - + open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeModel(label, forKey: .label) try container.encodeModelIfPresent(action, forKey: .action) + try container.encodeIfPresent(analyticsData, forKey: .analyticsData) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift index 631bf50f..3bb8ce43 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift @@ -66,6 +66,11 @@ extension TabsTableViewCell: TabsDelegate { MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model.tabs, additionalData: nil, delegateObject: delegateObject) } MVMCoreUIActionHandler.performActionUnstructured(with: SwapMoleculesActionModel(index < previousTabIndex ? .left : .right), sourceModel: model, additionalData: nil, delegateObject: delegateObject) + + if let analyticsData = try? model.tabs.tabs[index].analyticsData?.toJSONAny(), + let controller = self.delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol { + MVMCoreUILoggingHandler.shared()?.defaultLogPageUpdate(forController: controller, from: ["analyticsData": analyticsData]) + } } } From f74bea64c2b8535099470ac325cf4713ef6649ad Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 17 May 2024 21:24:58 -0400 Subject: [PATCH 32/64] Digital PCT265 story PCT-135: Switch to shallow equals with deep compare on parent in order to pinpoint midmatched models. Unit testing setup. --- MVMCoreUI.xcodeproj/project.pbxproj | 158 ++++ .../xcshareddata/xcschemes/MVMCoreUI.xcscheme | 67 ++ .../Atoms/Buttons/ButtonGroupModel.swift | 12 + .../Atomic/Atoms/Buttons/ButtonModel.swift | 2 +- MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift | 14 + .../Atomic/Atoms/Views/ImageViewModel.swift | 16 + .../Label/LabelAttributeActionModel.swift | 4 - .../Views/Label/LabelAttributeModel.swift | 6 + .../Atomic/Atoms/Views/Label/LabelModel.swift | 6 +- .../LockUps/TitleLockupModel.swift | 11 - .../MoleculeSectionHeaderModel.swift | 5 + .../TabsModel.swift | 2 +- .../TwoButtonViewModel.swift | 12 +- .../Items/AccordionListItemModel.swift | 8 + .../Molecules/Items/ListItemModel.swift | 4 +- .../Items/MoleculeCollectionItemModel.swift | 2 +- .../Items/MoleculeListItemModel.swift | 10 - .../Items/MoleculeStackItemModel.swift | 9 +- .../Molecules/Items/TabsListItemModel.swift | 13 +- .../Buttons/NavigationImageButtonModel.swift | 19 + .../MoleculeContainerModel.swift | 3 +- .../EyebrowHeadlineBodyLinkModel.swift | 5 +- .../HeadlineBodyModel.swift | 14 +- .../Atomic/Organisms/Carousel/Carousel.swift | 2 +- .../Organisms/Carousel/CarouselModel.swift | 6 +- MVMCoreUI/Atomic/Organisms/StackModel.swift | 10 +- .../MoleculeComparisonProtocol.swift | 12 +- .../MoleculeModelProtocol.swift | 3 +- .../ParentMoleculeModelProtocol.swift | 57 +- .../TemplateModelProtocol.swift | 2 +- .../Atomic/Templates/BaseTemplateModel.swift | 14 + .../Templates/ListPageTemplateModel.swift | 10 + .../Templates/MoleculeListTemplate.swift | 2 + .../Templates/ThreeLayerModelBase.swift | 8 + .../BaseControllers/ViewController.swift | 13 +- .../JSON/Modelling/UAD_page_model.json | 807 ++++++++++++++++++ .../JSON/Modelling/UAD_page_model_2.json | 807 ++++++++++++++++++ MVMCoreUITests/MVMCoreUITests.swift | 212 +++++ MVMCoreUITests/TestUtils.swift | 19 + 39 files changed, 2261 insertions(+), 125 deletions(-) create mode 100644 MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme create mode 100644 MVMCoreUITests/JSON/Modelling/UAD_page_model.json create mode 100644 MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json create mode 100644 MVMCoreUITests/MVMCoreUITests.swift create mode 100644 MVMCoreUITests/TestUtils.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 36641951..4893b1d1 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -170,6 +170,11 @@ 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; 5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; }; 5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */; }; + 583335592BF64E77001D90D7 /* MVMCoreUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */; }; + 5833355A2BF64E77001D90D7 /* MVMCoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */; }; + 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335622BF6509C001D90D7 /* UAD_page_model.json */; }; + 583335652BF6A5C3001D90D7 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583335642BF6A5C3001D90D7 /* TestUtils.swift */; }; + 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */; }; 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; }; 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; }; 58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */; }; @@ -610,6 +615,16 @@ FD99130028E21E4900542CC3 /* RuleNotEqualsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5833355B2BF64E77001D90D7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D29DF0C321E404D4003B2FB9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D29DF0CB21E404D4003B2FB9; + remoteInfo = MVMCoreUI; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 01004F2F22721C3800991ECC /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; 0103B84D23D7E33A009C315C /* HeadlineBodyToggleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyToggleModel.swift; sourceTree = ""; }; @@ -777,6 +792,11 @@ 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = ""; }; 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = ""; }; + 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MVMCoreUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUITests.swift; sourceTree = ""; }; + 583335622BF6509C001D90D7 /* UAD_page_model.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model.json; sourceTree = ""; }; + 583335642BF6A5C3001D90D7 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UAD_page_model_2.json; sourceTree = ""; }; 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = ""; }; 5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = ""; }; 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = ""; }; @@ -1223,6 +1243,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 583335532BF64E77001D90D7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5833355A2BF64E77001D90D7 /* MVMCoreUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0C921E404D4003B2FB9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1507,6 +1535,33 @@ path = Accessibility; sourceTree = ""; }; + 583335572BF64E77001D90D7 /* MVMCoreUITests */ = { + isa = PBXGroup; + children = ( + 583335602BF65063001D90D7 /* JSON */, + 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */, + 583335642BF6A5C3001D90D7 /* TestUtils.swift */, + ); + path = MVMCoreUITests; + sourceTree = ""; + }; + 583335602BF65063001D90D7 /* JSON */ = { + isa = PBXGroup; + children = ( + 583335612BF6506C001D90D7 /* Modelling */, + ); + path = JSON; + sourceTree = ""; + }; + 583335612BF6506C001D90D7 /* Modelling */ = { + isa = PBXGroup; + children = ( + 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */, + 583335622BF6509C001D90D7 /* UAD_page_model.json */, + ); + path = Modelling; + sourceTree = ""; + }; 8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = { isa = PBXGroup; children = ( @@ -2016,6 +2071,7 @@ isa = PBXGroup; children = ( D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */, + 583335572BF64E77001D90D7 /* MVMCoreUITests */, D29DF0CD21E404D4003B2FB9 /* Products */, D29DF0E421E4F3C7003B2FB9 /* Frameworks */, ); @@ -2025,6 +2081,7 @@ isa = PBXGroup; children = ( D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */, + 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */, ); name = Products; sourceTree = ""; @@ -2601,6 +2658,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 583335552BF64E77001D90D7 /* MVMCoreUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5833355F2BF64E77001D90D7 /* Build configuration list for PBXNativeTarget "MVMCoreUITests" */; + buildPhases = ( + 583335522BF64E77001D90D7 /* Sources */, + 583335532BF64E77001D90D7 /* Frameworks */, + 583335542BF64E77001D90D7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5833355C2BF64E77001D90D7 /* PBXTargetDependency */, + ); + name = MVMCoreUITests; + productName = MVMCoreUITests; + productReference = 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */ = { isa = PBXNativeTarget; buildConfigurationList = D29DF0D421E404D4003B2FB9 /* Build configuration list for PBXNativeTarget "MVMCoreUI" */; @@ -2625,9 +2700,13 @@ D29DF0C321E404D4003B2FB9 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1320; ORGANIZATIONNAME = "Verizon Wireless"; TargetAttributes = { + 583335552BF64E77001D90D7 = { + CreatedOnToolsVersion = 15.4; + }; D29DF0CB21E404D4003B2FB9 = { CreatedOnToolsVersion = 10.1; LastSwiftMigration = 1010; @@ -2650,11 +2729,21 @@ projectRoot = ""; targets = ( D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */, + 583335552BF64E77001D90D7 /* MVMCoreUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 583335542BF64E77001D90D7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */, + 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0CA21E404D4003B2FB9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2673,6 +2762,15 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 583335522BF64E77001D90D7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 583335592BF64E77001D90D7 /* MVMCoreUITests.swift in Sources */, + 583335652BF6A5C3001D90D7 /* TestUtils.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0C821E404D4003B2FB9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3246,6 +3344,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5833355C2BF64E77001D90D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */; + targetProxy = 5833355B2BF64E77001D90D7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ D29DF32821EE8736003B2FB9 /* Localizable.strings */ = { isa = PBXVariantGroup; @@ -3260,6 +3366,49 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 5833355D2BF64E77001D90D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vzw.MVMCoreUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5833355E2BF64E77001D90D7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vzw.MVMCoreUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; D29DF0D221E404D4003B2FB9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */; @@ -3452,6 +3601,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5833355F2BF64E77001D90D7 /* Build configuration list for PBXNativeTarget "MVMCoreUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5833355D2BF64E77001D90D7 /* Debug */, + 5833355E2BF64E77001D90D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D29DF0C621E404D4003B2FB9 /* Build configuration list for PBXProject "MVMCoreUI" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme b/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme new file mode 100644 index 00000000..97460f91 --- /dev/null +++ b/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift index 505e112d..028c9f22 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift @@ -79,4 +79,16 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(childWidthValue, forKey: .childWidthValue) try container.encodeIfPresent(childWidthPercentage, forKey: .childWidthPercentage) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return buttons.count == model.buttons.count + && surface == model.surface + && enabled == model.enabled + && alignment == model.alignment + && rowQuantityPhone == model.rowQuantityPhone + && rowQuantityTablet == model.rowQuantityTablet + && childWidthValue == model.childWidthValue + && childWidthPercentage == model.childWidthPercentage + } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 8cc0bdda..ffeb8c36 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -192,7 +192,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat return title == model.title && enabled == model.enabled && inverted == model.inverted - && action.isEqual(to: model.action) && accessibilityText == model.accessibilityText && accessibilityIdentifier == model.accessibilityIdentifier && accessibilityTraits == model.accessibilityTraits @@ -201,6 +200,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat && size == model.size && groupName == model.groupName && width == model.width + && action.isEqual(to: model.action) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift index 7697143e..4970ce24 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift @@ -125,5 +125,19 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { try container.encode(width, forKey: .width) try container.encode(height, forKey: .height) try container.encode(enabled, forKey: .enabled) + try container.encode(inverted, forKey: .inverted) + } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && disabledColor == model.disabledColor + && color == model.color + && degrees == model.degrees + && lineWidth == model.lineWidth + && width == model.width + && height == model.height + && enabled == model.enabled + && inverted == model.inverted } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift index 5079fc93..3a827cbe 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift @@ -61,4 +61,20 @@ case shouldMaskRecordedView case allowServerParameters } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && image == model.image + && accessibilityText == model.accessibilityText + && fallbackImage == model.fallbackImage + && imageFormat == model.imageFormat + && width == model.width + && height == model.height + && contentMode == model.contentMode + && cornerRadius == model.cornerRadius + && clipsImage == model.clipsImage + && allowServerParameters == model.allowServerParameters + && shouldMaskRecordedView == model.shouldMaskRecordedView + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift index 1dbac837..a4db3038 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift @@ -53,8 +53,4 @@ open class LabelAttributeActionModel: LabelAttributeModel { guard super.isEqual(to: model), let model = model as? Self else { return false } return action.isEqual(to: model.action) } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - return super.isEqual(to: model) - } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index 6da10fcc..df60e6ad 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -81,4 +81,10 @@ return location == model.location && length == model.length } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return location == model.location + && length == model.length + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index e3d37cf4..20e9d501 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -137,20 +137,20 @@ import VDS guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && text == model.text - && accessibilityText == model.accessibilityText && textColor == model.textColor && fontStyle == model.fontStyle && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.isEqual(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable && numberOfLines == model.numberOfLines + && accessibilityText == model.accessibilityText && accessibilityTraits == model.accessibilityTraits && inverted == inverted && shouldMaskRecordedView == model.shouldMaskRecordedView + && attributes.isEqual(to: model.attributes) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { @@ -162,7 +162,6 @@ import VDS && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.isVisuallyEquivalent(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable @@ -170,6 +169,7 @@ import VDS && accessibilityText == model.accessibilityText && accessibilityTraits == model.accessibilityTraits && inverted == inverted + && attributes.isVisuallyEquivalent(to: model.attributes) } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 14b0969d..e757fb3b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -53,17 +53,6 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { && alignment == model.alignment && inverted == model.inverted && backgroundColor == model.backgroundColor - && children.isEqual(to: model.children) - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return textAlignment == model.textAlignment - && subTitleColor == model.subTitleColor - && alignment == model.alignment - && inverted == model.inverted - && backgroundColor == model.backgroundColor - && children.isVisuallyEquivalent(to: model.children) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift index 12201722..a060dc2e 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift @@ -68,4 +68,9 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(line, forKey: .line) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return line.matchExistence(with: model.line) + } } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index 9f9ef766..3ea8a458 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -118,7 +118,7 @@ open class TabsModel: MoleculeModelProtocol { && borderLine == model.borderLine && minWidth == model.minWidth && selectedIndex == model.selectedIndex - && tabs.isEqual(to: model.tabs) + && tabs == model.tabs } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 2d978f17..5f7b423c 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -91,15 +91,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && fillContainer == model.fillContainer - && primaryButton.isEqual(to: model.primaryButton) - && secondaryButton.isEqual(to: model.secondaryButton) - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && fillContainer == model.fillContainer - && primaryButton.isVisuallyEquivalent(to: model.primaryButton) - && secondaryButton.isVisuallyEquivalent(to: model.secondaryButton) + && primaryButton.matchExistence(with: model.primaryButton) + && secondaryButton.matchExistence(with: model.secondaryButton) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift index 1d07a6f5..8100c72b 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift @@ -80,6 +80,14 @@ class AccordionListItemModel: MoleculeListItemModel { try container.encodeModelIfPresent(expandAction, forKey: .expandAction) try container.encodeModelIfPresent(collapseAction, forKey: .collapseAction) } + + override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return selected == model.selected + && hideLineWhenExpanded == model.hideLineWhenExpanded + && expandAction.matchExistence(with: model.expandAction) + && collapseAction.matchExistence(with: model.collapseAction) + } } extension AccordionListItemModel: PageBehaviorProtocolRequirer { diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index bf629f7a..0128c41e 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -140,7 +140,7 @@ import MVMCore && accessibilityText == model.accessibilityText && accessibilityValue == model.accessibilityValue && accessibilityTraits == model.accessibilityTraits - && line.isEqual(to: model.line) + && line.matchExistence(with: model.line) && action.isEqual(to: model.action) } @@ -153,6 +153,6 @@ import MVMCore && accessibilityText == model.accessibilityText && accessibilityValue == model.accessibilityValue && accessibilityTraits == model.accessibilityTraits - && line.isVisuallyEquivalent(to: model.line) + && line.matchExistence(with: model.line) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 9bfa69c0..945b4b49 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -82,6 +82,6 @@ public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return border == border + return border == model.border } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift index 82958a12..9e230607 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift @@ -52,14 +52,4 @@ import MVMCore try container.encode(moleculeName, forKey: .moleculeName) try container.encodeModel(molecule, forKey: .molecule) } - - public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard super.isEqual(to: model), let model = model as? Self else { return false } - return molecule.isEqual(to: model) - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return molecule.isVisuallyEquivalent(to: model.molecule) - } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift index 72b9ee24..1f17a3e7 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift @@ -60,14 +60,7 @@ } public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard super.isEqual(to: model), let model = model as? Self else { return false } - return spacing == model.spacing - && percent == model.percent - && gone == model.gone - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + guard super.isEqual(to: model), let model = model as? Self else { return false } return spacing == model.spacing && percent == model.percent && gone == model.gone diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index a769eb89..943f53ad 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -20,7 +20,7 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]? public var children: [MoleculeModelProtocol] { - return molecules.flatMap { $0 } + return [tabs] + molecules.flatMap { $0 } } public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { @@ -94,16 +94,7 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } - return tabs.isEqual(to: model.tabs) - && molecules.count == model.molecules.count - && zip(molecules, model.molecules).allSatisfy({ $0.0.isEqual(to: $0.1) }) - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return tabs.isVisuallyEquivalent(to: model.tabs) - && molecules.count == model.molecules.count - && zip(molecules, model.molecules).allSatisfy({ $0.0.isVisuallyEquivalent(to: $0.1) }) + return molecules.count == model.molecules.count } } diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift index ab1c993e..d30ba5d7 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift @@ -73,6 +73,25 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule try container.encodeIfPresent(imageRenderingMode, forKey: .imageRenderingMode) } + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return true } + return backgroundColor == model.backgroundColor + && accessibilityIdentifier == model.accessibilityIdentifier + && image == model.image + && accessibilityText == model.accessibilityText + && imageRenderingMode == model.imageRenderingMode + && action.isEqual(to: action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return true } + return backgroundColor == model.backgroundColor + && accessibilityIdentifier == model.accessibilityIdentifier + && image == model.image + && accessibilityText == model.accessibilityText + && imageRenderingMode == model.imageRenderingMode + } + //-------------------------------------------------- // MARK: - Method //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index a31f801b..a97f4af6 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -64,8 +64,7 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } - return molecule.isEqual(to: model.molecule) - && backgroundColor == model.backgroundColor + return backgroundColor == model.backgroundColor } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index bc9dad7f..14008a5d 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -110,6 +110,9 @@ public class EyebrowHeadlineBodyLinkModel: ParentMoleculeModelProtocol { public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && children.isEqual(to: model.children) + && eyebrow.matchExistence(with: model.eyebrow) + && headline.matchExistence(with: model.headline) + && body.matchExistence(with: model.body) + && link.matchExistence(with: model.link) } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index 1c2e03a7..80893c44 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -96,18 +96,8 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } - return headline.isEqual(to: model.headline) - && body.isEqual(to: model.body) - && style == style - && backgroundColor == backgroundColor - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return headline.isVisuallyEquivalent(to: model.headline) - && body.isVisuallyEquivalent(to: model.body) - && style == style - && backgroundColor == backgroundColor + return style == style + && backgroundColor == model.backgroundColor } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index acd44595..12399292 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -174,7 +174,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.isVisuallyEquivalent(to: originalModel) { + if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel) { // 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/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 1ef5b98f..42c8363c 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -175,14 +175,12 @@ import UIKit public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == backgroundColor - && molecules.isEqual(to: model.molecules) && spacing == model.spacing && border == model.border && loop == model.loop && height == model.height && itemWidthPercent == model.itemWidthPercent && itemAlignment == model.itemAlignment - && pagingMolecule.isEqual(to: model.pagingMolecule) && paging == model.paging && useHorizontalMargins == model.useHorizontalMargins && leftPadding == model.leftPadding @@ -193,6 +191,8 @@ import UIKit && groupName == model.groupName && enabled == model.enabled && readOnly == model.readOnly + && molecules.count == model.molecules.count + && pagingMolecule.matchExistence(with: model.pagingMolecule) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { @@ -212,8 +212,6 @@ import UIKit && baseValue == model.baseValue && enabled == model.enabled && readOnly == model.readOnly - && pagingMolecule.isVisuallyEquivalent(to: model.pagingMolecule) - && molecules.isVisuallyEquivalent(to: model.molecules) } } diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 65baa962..3b687c18 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -82,16 +82,8 @@ public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && molecules.isEqual(to: model.molecules) - && axis == model.axis - && spacing == model.spacing - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && molecules.isVisuallyEquivalent(to: model.molecules) && axis == model.axis && spacing == model.spacing + && molecules.count == model.molecules.count } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 5465c8e4..5e47e33f 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -11,7 +11,7 @@ import Foundation public protocol MoleculeModelComparisonProtocol: ModelComparisonProtocol { /** - True if there are no visual differences between models. + Shallow check if there are no visual differences between models. By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. **/ @@ -25,6 +25,16 @@ extension MoleculeModelComparisonProtocol { } } +public extension MoleculeModelComparisonProtocol where Self: ParentModelProtocol { + func isDeeplyVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return !findFirst(in: model, failing: { mine, theirs in + guard let mine = mine as? MoleculeModelComparisonProtocol, let theirs = theirs as? MoleculeModelComparisonProtocol else { return false } + return mine.isVisuallyEquivalent(to: theirs) + }).matched + } +} + public extension Optional { /// Checks if the curent model is equal to another model. diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index bf55de18..3dc09d7c 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -21,7 +21,8 @@ public extension MoleculeModelProtocol { func isEqual(to model: ModelProtocol) -> Bool { guard let model = model as? Self else { return false } - return id == model.id + return moleculeName == model.moleculeName + && backgroundColor == model.backgroundColor } var debugDescription: String { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 25dbcdc7..88b09b86 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -67,16 +67,6 @@ public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelP public extension ParentMoleculeModelProtocol { - func isEqual(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return model.children.isEqual(to: model.children) - } - - func areVisuallyEquivalent(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return model.children.isVisuallyEquivalent(to: model.children) - } - func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int) -> Result) -> Result { var result = initialResult if (options == .parentFirst) { @@ -122,53 +112,66 @@ public extension ParentMoleculeModelProtocol { } } -extension ParentModelProtocol { +public extension ParentModelProtocol { - public typealias DeepCompareResult = (matched: Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?) + typealias DeepCompareResult = (matched: Bool, mine: ModelProtocol?, theirs: ModelProtocol?) - public typealias ModelPair = (mine: ModelProtocol, theirs: ModelProtocol) + 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 findFirst(in: model) { $0.isEqual(to: $1) } + func deepEquals(to model: any ModelProtocol) -> Bool { + guard let model = model as? ParentModelProtocol else { return false } + return !findFirst(in: model, failing: { $0.isEqual(to: $1) }).matched } - func findFirst(in anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> DeepCompareResult { + func findFirst(in anotherParent: ParentModelProtocol, failing test: (ModelProtocol, ModelProtocol)->Bool, options: TreeTraversalOptions = .childFirst) -> DeepCompareResult { - guard test(self, anotherParent) else { return (false, myChild: self, theirChild: anotherParent)} + guard options != .parentFirst, test(self, anotherParent) else { return (true, mine: self, theirs: anotherParent)} let myChildren = children let theirChildren = anotherParent.children - guard myChildren.count == theirChildren.count else { return (false, myChild: self, theirChild: self) } + guard myChildren.count == theirChildren.count else { return (true, mine: self, theirs: anotherParent) } for index in myChildren.indices { if let myChild = myChildren[index] as? ParentModelProtocol { if let theirChild = theirChildren[index] as? ParentModelProtocol { - let result = myChild.findFirst(in: theirChild, where: test) - guard result.0 else { return result } + let result = myChild.findFirst(in: theirChild, failing: test) + guard !result.0 else { return result } } else { - return (false, myChild: myChild, theirChild: theirChildren[index]) + return (true, mine: myChild, theirs: theirChildren[index]) } } else if !test(myChildren[index], theirChildren[index]) { - return (false, myChild: myChildren[index], theirChild: theirChildren[index]) + return (true, mine: myChildren[index], theirs: theirChildren[index]) } } - return (true, nil, nil) + guard options == .childFirst, test(self, anotherParent) else { return (true, mine: self, theirs: anotherParent)} + + return (false, nil, nil) + } + + func findAllTheirsNotEqual(against anotherParent: ParentModelProtocol) -> [T] { + return deepCompare(against: anotherParent) { $0.isEqual(to: $1) }.compactMap { $0.theirs as? T } + } + + func findAllNotEqual(against anotherParent: ParentModelProtocol) -> [ModelPair] { + return deepCompare(against: anotherParent) { $0.isEqual(to: $1) } } func deepCompare(against anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> [ModelPair] { - guard test(self, anotherParent) else { return [(self, anotherParent)]} + var allDiffs = [ModelPair]() let myChildren = children let theirChildren = anotherParent.children guard myChildren.count == theirChildren.count else { return [(self, anotherParent)] } - var allDiffs = [ModelPair]() + if !test(self, anotherParent) { + allDiffs.append((self, anotherParent)) + } + 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] + let childDiffs = myChild.deepCompare(against: theirChild, where: test) allDiffs.append(contentsOf: childDiffs) } else if !test(myChildren[index], theirChildren[index]) { allDiffs.append((myChildren[index], theirChildren[index])) diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 2ee8a04a..b598041b 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -7,7 +7,7 @@ // -public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol { +public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol, MoleculeModelComparisonProtocol { var template: String { get } var rootMolecules: [MoleculeModelProtocol] { get } /// Page rendering ID. Unique betwen JSON parses. diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index 78c7d61b..da57579d 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -121,4 +121,18 @@ import Foundation try container.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex) try container.encode(shouldMaskScreenWhileRecording, forKey: .shouldMaskScreenWhileRecording) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return pageType == model.pageType + && backgroundColor == model.backgroundColor + && screenHeading == model.screenHeading + && formRules?.count ?? 0 == model.formRules?.count ?? 0 + && navigationBar.matchExistence(with: model.navigationBar) + && tabBarHidden == model.tabBarHidden + && hideLeftPanel == model.hideLeftPanel + && hideRightPanel == model.hideRightPanel + && tabBarIndex == model.tabBarIndex + && shouldMaskScreenWhileRecording == model.shouldMaskScreenWhileRecording + } } diff --git a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift index 5671013b..9af7b41d 100644 --- a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift @@ -100,4 +100,14 @@ try container.encodeIfPresent(footerlessSpacerColor, forKey: .footerlessSpacerColor) try container.encodeIfPresent(footerlessSpacerHeight, forKey: .footerlessSpacerHeight) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return line.matchExistence(with: model.line) + && scrollToRowIndex == model.scrollToRowIndex + && singleCellSelection == model.singleCellSelection + && footerlessSpacerColor == model.footerlessSpacerColor + && footerlessSpacerHeight == model.footerlessSpacerHeight + && molecules?.count ?? 0 == model.molecules?.count ?? 0 + } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index ef382c0f..197701b0 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -270,6 +270,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol return IndexPath(row: $0, section: 0) } + debugLog("Refreshing rows \(indexPaths.map { $0.row })") + if #available(iOS 15.0, *) { // All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to levearage this more efficient method. tableView.reconfigureRows(at: indexPaths) diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift index 921e74ab..481f8cad 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift @@ -77,4 +77,12 @@ try container.encodeIfPresent(anchorFooter, forKey: .anchorFooter) try container.encodeModelIfPresent(footer, forKey: .footer) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return anchorHeader == model.anchorHeader + && anchorFooter == model.anchorFooter + && header.matchExistence(with: model.header) + && footer.matchExistence(with: model.footer) + } } diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 56b58b89..50aa1df2 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -40,7 +40,7 @@ import MVMCore public var needsUpdateUI = false private var observingForResponses: NSObjectProtocol? private var initialLoadFinished = false - private var isFirstRender = true + public var isFirstRender = true public var previousScreenSize = CGSize.zero public var selectedField: UIView? @@ -259,7 +259,12 @@ import MVMCore 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. + // Only recognize the molecules that actually changed. + if let replaced = replaced as? ParentMoleculeModelProtocol, let molecule = molecule as? ParentMoleculeModelProtocol { + let diffs: [MoleculeModelProtocol] = replaced.findAllTheirsNotEqual(against: molecule) + debugLog("Behavior updated \(diffs) in template model.") + behaviorUpdatedModels.append(contentsOf: diffs) + } else if !replaced.isEqual(to: molecule) { 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.) } @@ -289,7 +294,7 @@ import MVMCore !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 } + pageUpdatedModels = diffs.compactMap { $0.theirs as? MoleculeModelProtocol } } let allUpdatedMolecules = isFirstRender ? [] : behaviorUpdatedModels + pageUpdatedModels @@ -303,7 +308,7 @@ import MVMCore debugLog("Performing full page render...") updateUI() } else { - debugLog("Updating \(allUpdatedMolecules) molecules...") + debugLog("Performing partial render of \(allUpdatedMolecules) molecules...") updateUI(for: allUpdatedMolecules) } diff --git a/MVMCoreUITests/JSON/Modelling/UAD_page_model.json b/MVMCoreUITests/JSON/Modelling/UAD_page_model.json new file mode 100644 index 00000000..aa880d36 --- /dev/null +++ b/MVMCoreUITests/JSON/Modelling/UAD_page_model.json @@ -0,0 +1,807 @@ +{ + "template": "list", + "analyticsData": { + "vzdl.user.customerBusiness": "joint_cust", + "vzdl.target.engagement.intent": "account management", + "vzdl.page.feedCardImpression": "L1|P1|greetingSection|Edit profile & settings^L1|P1|mobileAccountSection|BillsTile^L1|P2|mobileAccountSection|UsageTile^L1|P3|mobileAccountSection|OrdersTile^L1|P4|mobileAccountSection|PlansTile^L1|P5|mobileAccountSection|Services_perksTile^L1|P6|mobileAccountSection|Account_activityTile^L1|P1|fivegHomeAccountSection|BillsTile^L1|P2|fivegHomeAccountSection|OrdersTile^L1|P3|fivegHomeAccountSection|PlansTile^L1|P4|fivegHomeAccountSection|Account_activityTile^L1|P5|fivegHomeAccountSection|Services_perksTile^L1|P1|fiosAccountSection|BillsTile^L1|P2|fiosAccountSection|PlansTile^L1|P3|fiosAccountSection|SupportTile^L1|P4|fiosAccountSection|Account_activityTile^L1|P5|fiosAccountSection|Home_offersTile", + "vzdl.user.id": "a3b00d5af5c7b5d26d017023f12dfa791dba533a33a3ed264c8c98237ae5902f", + "vzdl.user.account": "c0d793b51b000cce8d6ec062a71a224d0faaf8e50c36ebd31240539b31aca001", + "vzdl.user.accountType": "postpaid", + "vzdl.events.uadcardserved": "1", + "vzdl.page.channelSession": "4154e060-85ae-4e8f-a371-fb00c618797a", + "vzdl.page.siteSection": "mva_atomic", + "vzdl.page.sourceChannel": "mva", + "vzdl.page.flow": "account overview", + "vzdl.env.businessUnit": "wireless", + "vzdl.page.displayChannel": "mva", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.id": "atomicAccountLanding" + }, + "molecules": [ + { + "useVerticalMargins": false, + "backgroundColor": "black", + "moleculeName": "tabsListItem", + "tabs": { + "borderLine": false, + "moleculeName": "tabs", + "tabs": [ + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Mobile" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "5G Home" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Fios" + } + } + ], + "style": "dark", + "selectedIndex": 1 + }, + "molecules": [ + [ + { + "id": "priorityTiles", + "line": { + "type": "none" + }, + "backgroundColor": "black", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 160, + "molecules": [ + { + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "topPadding": 16, + "useHorizontalMargins": false, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + } + ], + "itemWidthPercent": 100, + "accessibilityText": "carousel", + "spacing": 12, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20, + "inverted": true + }, + "rightPadding": 16, + "leftPadding": 16 + }, + "gone": true + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Bills", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|mobileAccountSection|BillsTile", + "vzdl.page.linkName": "Mobile|Bills", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/bill/overview/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Usage", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|mobileAccountSection|UsageTile", + "vzdl.page.linkName": "Mobile|Usage", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "sourcePage": "plansAndDeviceLanding", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "dataUsageDetails" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Orders", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|mobileAccountSection|OrdersTile", + "vzdl.page.linkName": "Mobile|Orders", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/orders/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Plans", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P4|mobileAccountSection|PlansTile", + "vzdl.page.linkName": "Mobile|Plans", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/cpc/mvm?pmd=y&tabbar=true", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Services & perks", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P5|mobileAccountSection|Services_perksTile", + "vzdl.page.linkName": "Mobile|Services & perks", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/products/producthub/home", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "id": "notification_count", + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Account activity", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P6|mobileAccountSection|Account_activityTile", + "vzdl.page.linkName": "Mobile|Account activity", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/optg/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "moleculeName": "listItem", + "line": { + "moleculeName": "line", + "type": "none" + }, + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Devices", + "fontStyle": "BoldTitleMedium" + }, + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Manage all devices", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/devices/landing/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Manage all devices" + }, + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "moleculeName": "listItem", + "style": "none", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "moleculeName": "carouselItem", + "molecule": { + "molecule": { + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Add a line and get select phones on us", + "fontStyle": "RegularMicro" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularMicro", + "numberOfLines": 1, + "moleculeName": "label", + "text": "\n\n\n\n\n" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Shop", + "moleculeName": "button", + "action": { + "extraParameters": { + "browserUrl": "sales/next/shop.html?flow=AAL&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "center" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Bring my own", + "moleculeName": "link", + "action": { + "extraParameters": { + "browserUrl": "sales/digital/byod.html?flow=NSO&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "fill", + "topPadding": 12, + "image": { + "image": "https://ss7.vzw.com/is/image/VerizonWireless/background-image-mobile?&scl=2", + "moleculeName": "image" + }, + "moleculeName": "bgImageContainer", + "bottomPadding": 12 + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 48, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "label", + "text": "Make the most of your plans", + "fontStyle": "BoldTitleMedium" + } + }, + { + "id": "forYouTiles", + "style": "none", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Discover more", + "fontStyle": "BoldTitleMedium" + }, + "verticalAlignment": "center", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Mobile|Shop all", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "locale": "EN", + "browserUrl": "sales/digital/shoplanding.html?isShopFlow=true&entrypoint=tabbar", + "requestFrom": "Shop" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Shop all" + }, + "verticalAlignment": "center", + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "id": "discoverTiles", + "style": "none", + "moleculeName": "listItem", + "bottomPadding": 32, + "useVerticalMargins": true, + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + } + ] + ] + } + ], + "backgroundColor": "black", + "hab": { + "configuration": "single", + "inverted": false + }, + "cache": true, + "header": { + "useVerticalMargins": true, + "backgroundColor": "black", + "moleculeName": "stack", + "bottomPadding": 12, + "molecules": [ + { + "moleculeName": "stackItem", + "id": "greetingSection", + "molecule": { + "fontStyle": "RegularTitleMedium", + "textColor": "#FFFFFF", + "moleculeName": "label", + "text": "Hi Lebowski." + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "enabledColor": "white", + "title": "Edit profile & settings", + "moleculeName": "link", + "inverted": true, + "action": { + "actionType": "openPage", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|greetingSection|Edit profile & settings", + "vzdl.page.linkName": "Edit profile & settings", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "pageType": "oneVzIdSettingsLanding", + "presentationStyle": "push", + "requestURL": "https://mobile-exp-qa2.vzw.com/mobile/nsa/nos/gw/launchapp/l2/webview", + "tryToReplaceFirst": false, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/acct/unifiedprofile/overview" + } + } + } + } + ] + }, + "behaviors": [ + { + "moleculeIds": [ + "priorityTiles", + "forYouTiles", + "discoverTiles", + "priorityTiles5G", + "forYouTiles5G", + "discoverTiles5G", + "notification_count", + "priorityTilesFIOS", + "phoneServiceFIOS", + "internetServiceFIOS", + "tvServiceFIOS", + "forYouTilesFIOS", + "discoverTilesFIOS", + "FiosBills", + "FiosSubBills", + "FiosPlans", + "FiosSupport", + "notification_count_fios", + "greetingSection", + "FiosHomeOffers", + "priorityTilesLTE", + "forYouTilesLTE", + "discoverTilesLTE" + ], + "behaviorName": "replaceMoleculeBehavior" + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/topCardsMVA", + "pageType": "topCardsMVA" + } + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/bottomCardsMVA", + "pageType": "bottomCardsMVA" + } + } + ], + "pageType": "atomicAccountLanding", + "loggedInMdn": "2815434851", + "presentationStyle": "root", + "footerlessSpacerColor": "white", + "tabBarIndex": 1, + "navigationBar": { + "style": "dark", + "moleculeName": "navigationBar", + "additionalLeftButtons": [ + { + "accessibilityText": "Verizon logo button, tap anytime to scroll to top of page", + "moleculeName": "navigationImageButton", + "image": "nav_vz_mark", + "imageRenderingMode": "alwaysOriginal", + "action": { + "actionType": "noop" + } + } + ], + "additionalRightButtons": [ + { + "accessibilityText": "Stores", + "moleculeName": "navigationImageButton", + "image": "nav_stores_white", + "action": { + "analyticsData": { + "vzdl.page.linkName": "global tab nav:stores" + }, + "title": "Stores", + "actionType": "openPage", + "pageType": "rtlStoreJourney" + } + } + ] + }, + "footerlessSpacerHeight": 0 +} diff --git a/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json b/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json new file mode 100644 index 00000000..3d292f72 --- /dev/null +++ b/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json @@ -0,0 +1,807 @@ +{ + "template": "list", + "analyticsData": { + "vzdl.user.customerBusiness": "joint_cust", + "vzdl.target.engagement.intent": "account management", + "vzdl.page.feedCardImpression": "L1|P1|greetingSection|Edit profile & settings^L1|P1|mobileAccountSection|BillsTile^L1|P2|mobileAccountSection|UsageTile^L1|P3|mobileAccountSection|OrdersTile^L1|P4|mobileAccountSection|PlansTile^L1|P5|mobileAccountSection|Services_perksTile^L1|P6|mobileAccountSection|Account_activityTile^L1|P1|fivegHomeAccountSection|BillsTile^L1|P2|fivegHomeAccountSection|OrdersTile^L1|P3|fivegHomeAccountSection|PlansTile^L1|P4|fivegHomeAccountSection|Account_activityTile^L1|P5|fivegHomeAccountSection|Services_perksTile^L1|P1|fiosAccountSection|BillsTile^L1|P2|fiosAccountSection|PlansTile^L1|P3|fiosAccountSection|SupportTile^L1|P4|fiosAccountSection|Account_activityTile^L1|P5|fiosAccountSection|Home_offersTile", + "vzdl.user.id": "a3b00d5af5c7b5d26d017023f12dfa791dba533a33a3ed264c8c98237ae5902f", + "vzdl.user.account": "c0d793b51b000cce8d6ec062a71a224d0faaf8e50c36ebd31240539b31aca001", + "vzdl.user.accountType": "postpaid", + "vzdl.events.uadcardserved": "1", + "vzdl.page.channelSession": "4154e060-85ae-4e8f-a371-fb00c618797a", + "vzdl.page.siteSection": "mva_atomic", + "vzdl.page.sourceChannel": "mva", + "vzdl.page.flow": "account overview", + "vzdl.env.businessUnit": "wireless", + "vzdl.page.displayChannel": "mva", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.id": "atomicAccountLanding" + }, + "molecules": [ + { + "useVerticalMargins": false, + "backgroundColor": "black", + "moleculeName": "tabsListItem", + "tabs": { + "borderLine": false, + "moleculeName": "tabs", + "tabs": [ + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Mobile" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "5G Home" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Fios" + } + } + ], + "style": "dark", + "selectedIndex": 1 + }, + "molecules": [ + [ + { + "id": "priorityTiles", + "line": { + "type": "none" + }, + "backgroundColor": "black", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 160, + "molecules": [ + { + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "topPadding": 16, + "useHorizontalMargins": false, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + } + ], + "itemWidthPercent": 100, + "accessibilityText": "carousel", + "spacing": 12, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20, + "inverted": true + }, + "rightPadding": 16, + "leftPadding": 16 + }, + "gone": true + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Bills", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|mobileAccountSection|BillsTile", + "vzdl.page.linkName": "Mobile|Bills", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/bill/overview/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Usage", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|mobileAccountSection|UsageTile", + "vzdl.page.linkName": "Mobile|Usage", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "2", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "sourcePage": "plansAndDeviceLanding", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "dataUsageDetails" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Orders", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|mobileAccountSection|OrdersTile", + "vzdl.page.linkName": "Mobile|Orders", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/orders/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Plans", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P4|mobileAccountSection|PlansTile", + "vzdl.page.linkName": "Mobile|Plans", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/cpc/mvm?pmd=y&tabbar=true", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Services & perks", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P5|mobileAccountSection|Services_perksTile", + "vzdl.page.linkName": "Mobile|Services & perks", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/products/producthub/home", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "id": "notification_count", + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Account activity", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P6|mobileAccountSection|Account_activityTile", + "vzdl.page.linkName": "Mobile|Account activity", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/optg/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "moleculeName": "listItem", + "line": { + "moleculeName": "line", + "type": "none" + }, + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Devices", + "fontStyle": "BoldTitleMedium" + }, + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Manage all devices", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/devices/landing/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Manage all devices" + }, + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "moleculeName": "listItem", + "style": "none", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "moleculeName": "carouselItem", + "molecule": { + "molecule": { + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Add a line and get select phones on us", + "fontStyle": "RegularMicro" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularMicro", + "numberOfLines": 1, + "moleculeName": "label", + "text": "\n\n\n\n\n" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Shop", + "moleculeName": "button", + "action": { + "extraParameters": { + "browserUrl": "sales/next/shop.html?flow=AAL&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "center" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Bring my own", + "moleculeName": "link", + "action": { + "extraParameters": { + "browserUrl": "sales/digital/byod.html?flow=NSO&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "fill", + "topPadding": 12, + "image": { + "image": "https://ss7.vzw.com/is/image/VerizonWireless/background-image-mobile?&scl=2", + "moleculeName": "image" + }, + "moleculeName": "bgImageContainer", + "bottomPadding": 12 + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 48, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "label", + "text": "Make the most of your plans", + "fontStyle": "BoldTitleMedium" + } + }, + { + "id": "forYouTiles", + "style": "none", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Discover more", + "fontStyle": "BoldTitleMedium" + }, + "verticalAlignment": "center", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Mobile|Shop all", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "locale": "EN", + "browserUrl": "sales/digital/shoplanding.html?isShopFlow=true&entrypoint=tabbar", + "requestFrom": "Shop" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Shop all" + }, + "verticalAlignment": "center", + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "id": "discoverTiles", + "style": "none", + "moleculeName": "listItem", + "bottomPadding": 32, + "useVerticalMargins": true, + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + } + ] + ] + } + ], + "backgroundColor": "black", + "hab": { + "configuration": "single", + "inverted": false + }, + "cache": true, + "header": { + "useVerticalMargins": true, + "backgroundColor": "black", + "moleculeName": "stack", + "bottomPadding": 12, + "molecules": [ + { + "moleculeName": "stackItem", + "id": "greetingSection", + "molecule": { + "fontStyle": "RegularTitleMedium", + "textColor": "#FFFFFF", + "moleculeName": "label", + "text": "Hi Lebowski." + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "enabledColor": "white", + "title": "Edit profile & settings", + "moleculeName": "link", + "inverted": true, + "action": { + "actionType": "openPage", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|greetingSection|Edit profile & settings", + "vzdl.page.linkName": "Edit profile & settings", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "pageType": "oneVzIdSettingsLanding", + "presentationStyle": "push", + "requestURL": "https://mobile-exp-qa2.vzw.com/mobile/nsa/nos/gw/launchapp/l2/webview", + "tryToReplaceFirst": false, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/acct/unifiedprofile/overview" + } + } + } + } + ] + }, + "behaviors": [ + { + "moleculeIds": [ + "priorityTiles", + "forYouTiles", + "discoverTiles", + "priorityTiles5G", + "forYouTiles5G", + "discoverTiles5G", + "notification_count", + "priorityTilesFIOS", + "phoneServiceFIOS", + "internetServiceFIOS", + "tvServiceFIOS", + "forYouTilesFIOS", + "discoverTilesFIOS", + "FiosBills", + "FiosSubBills", + "FiosPlans", + "FiosSupport", + "notification_count_fios", + "greetingSection", + "FiosHomeOffers", + "priorityTilesLTE", + "forYouTilesLTE", + "discoverTilesLTE" + ], + "behaviorName": "replaceMoleculeBehavior" + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/topCardsMVA", + "pageType": "topCardsMVA" + } + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/bottomCardsMVA", + "pageType": "bottomCardsMVA" + } + } + ], + "pageType": "atomicAccountLanding", + "loggedInMdn": "2815434851", + "presentationStyle": "root", + "footerlessSpacerColor": "white", + "tabBarIndex": 1, + "navigationBar": { + "style": "dark", + "moleculeName": "navigationBar", + "additionalLeftButtons": [ + { + "accessibilityText": "Verizon logo button, tap anytime to scroll to top of page", + "moleculeName": "navigationImageButton", + "image": "nav_vz_mark", + "imageRenderingMode": "alwaysOriginal", + "action": { + "actionType": "noop" + } + } + ], + "additionalRightButtons": [ + { + "accessibilityText": "Stores", + "moleculeName": "navigationImageButton", + "image": "nav_stores_white", + "action": { + "analyticsData": { + "vzdl.page.linkName": "global tab nav:stores" + }, + "title": "Stores", + "actionType": "openPage", + "pageType": "rtlStoreJourney" + } + } + ] + }, + "footerlessSpacerHeight": 0 +} diff --git a/MVMCoreUITests/MVMCoreUITests.swift b/MVMCoreUITests/MVMCoreUITests.swift new file mode 100644 index 00000000..af631c93 --- /dev/null +++ b/MVMCoreUITests/MVMCoreUITests.swift @@ -0,0 +1,212 @@ +// +// MVMCoreUITests.swift +// MVMCoreUITests +// +// Created by Kyle Hedden on 5/16/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import XCTest +import MVMCoreUI +import MVMCore + +enum TestError: Error { + case resoureNotFound +} + +final class MVMCoreUITests: XCTestCase { + + override class func setUp() { + super.setUp() + CoreUIModelMapping.registerObjects() + } + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + //func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + //} + + func testModelShallowEquality() throws { + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + XCTAssertTrue(model1.isVisuallyEquivalent(to: model2)) + XCTAssertTrue(model1.isVisuallyEquivalent(to: model3)) + + XCTAssertTrue(LabelModel(text: "Hello World").isEqual(to: LabelModel(text: "Hello World"))) + XCTAssertFalse(LabelModel(text: "Hello World").isEqual(to: LabelModel(text: "Hello Moon"))) + } + + func testFindFirstCompare() throws { + + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + let result = model1.findFirst(in: model2) { model1, model2 in + model1.isEqual(to: model2) + } + XCTAssertTrue(result.matched) + XCTAssertEqual((result.theirs as? LabelModel)?.text, "Hello Moon") + } + + func testDeepCompare() throws { + + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + let attributedLabel = LabelModel(text: "Hello World") + attributedLabel.attributes = [LabelAttributeActionModel(0, 5, action: ActionNoopModel())] + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + attributedLabel + ) + ]) + + let attributedLabel2 = LabelModel(text: "Hello World") + attributedLabel2.attributes = [LabelAttributeActionModel(0, 5, action: ActionBackModel())] + let model4 = StackModel(molecules: [ + MoleculeStackItemModel(with: + attributedLabel2 + ) + ]) + + let results = model1.findAllNotEqual(against: model2) + XCTAssertEqual(results.count, 1) + XCTAssertEqual((results.first?.theirs as? LabelModel)?.text, "Hello Moon") + XCTAssertFalse(model1.deepEquals(to: model2)) + XCTAssertFalse(model1.isDeeplyVisuallyEquivalent(to: model2)) + XCTAssertFalse(model1.deepEquals(to: model3)) + + let visualResults = model3.findFirst(in: model4, failing: { mine, theirs in + guard let mine = mine as? MoleculeModelComparisonProtocol, let theirs = theirs as? MoleculeModelComparisonProtocol else { return false } + return mine.isVisuallyEquivalent(to: theirs) + }) + XCTAssertFalse(visualResults.matched) + XCTAssertTrue(model3.isDeeplyVisuallyEquivalent(to: model4)) + } + + func testDeepCompareReturnsAllResults() { + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ) + ]) + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + ) + ]) + + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + var results = model1.findAllNotEqual(against: model2) + XCTAssertEqual(results.count, 3) + + results = model1.findAllNotEqual(against: model3) + XCTAssertEqual(results.count, 3) + } + + func testPageEquality() throws { + let listTemplateModel1 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel2 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel3 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_2")) + + let results = listTemplateModel1.findFirst(in: listTemplateModel2, failing: { $0.isEqual(to: $1) }) + XCTAssertFalse(results.matched) + XCTAssertTrue(listTemplateModel1.deepEquals(to: listTemplateModel2)) + XCTAssertTrue(listTemplateModel1.isDeeplyVisuallyEquivalent(to: listTemplateModel2)) + + let results2 = listTemplateModel1.findFirst(in: listTemplateModel3, failing: { $0.isEqual(to: $1) }) + XCTAssertTrue(results2.matched) + XCTAssertFalse(listTemplateModel1.deepEquals(to: listTemplateModel3)) + XCTAssertTrue(listTemplateModel1.isDeeplyVisuallyEquivalent(to: listTemplateModel3)) + } + + func testPageEqualityPerformance() throws { + let listTemplateModel1 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel2 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_2")) + measure { + XCTAssertFalse(listTemplateModel1.deepEquals(to: listTemplateModel2)) + } + } + +} diff --git a/MVMCoreUITests/TestUtils.swift b/MVMCoreUITests/TestUtils.swift new file mode 100644 index 00000000..576a3231 --- /dev/null +++ b/MVMCoreUITests/TestUtils.swift @@ -0,0 +1,19 @@ +// +// TestUtils.swift +// MVMCoreUITests +// +// Created by Kyle Hedden on 5/16/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import XCTest + +extension XCTestCase { + + func getFileData(_ fileName: String, type: String = "json") throws -> Data { + guard let resourcePath = Bundle(identifier: "com.vzw.MVMCoreUITests")?.path(forResource: fileName, ofType: type) else { throw TestError.resoureNotFound } + return try Data(contentsOf: URL(fileURLWithPath: resourcePath)) + } + +} From 8e70aba1573780f9ec3646fbef70e9a43fc84271 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Mon, 20 May 2024 11:54:01 -0400 Subject: [PATCH 33/64] Digital PCT265 defect CXTDT-54658: Code review. --- MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift index 3bb8ce43..c97c0ed1 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift @@ -68,7 +68,7 @@ extension TabsTableViewCell: TabsDelegate { MVMCoreUIActionHandler.performActionUnstructured(with: SwapMoleculesActionModel(index < previousTabIndex ? .left : .right), sourceModel: model, additionalData: nil, delegateObject: delegateObject) if let analyticsData = try? model.tabs.tabs[index].analyticsData?.toJSONAny(), - let controller = self.delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol { + let controller = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol { MVMCoreUILoggingHandler.shared()?.defaultLogPageUpdate(forController: controller, from: ["analyticsData": analyticsData]) } } From 4a8ac3f9f061909d755a11980dcd0f379bb3aba5 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Mon, 20 May 2024 14:55:01 -0400 Subject: [PATCH 34/64] Digital ACT191 story ONEAPP-7459 - Update to allow separate models in the pattern navigation --- .../NavigationBar/NavigationItemModel.swift | 44 +++++++------------ .../NavigationItemModelProtocol.swift | 4 +- .../UINavigationController+Extension.swift | 8 ++-- ...MCoreUISplitViewController+Extension.swift | 2 +- 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 67fc779f..07d8f760 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -20,16 +20,14 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc open class var identifier: String { "navigationBar" } public var id: String = UUID().uuidString - - private let defaultHidesSystemBackButton = true - + open var title: String? - open var hidden = false - open var line: LineModel? = LineModel(type: .secondary) - open var hidesSystemBackButton = true + open var hidden: Bool? + open var line: LineModel? + open var hidesSystemBackButton: Bool? open var style: NavigationItemStyle? - - private var _backgroundColor: Color? + + open var _backgroundColor: Color? open var backgroundColor: Color? { get { if let backgroundColor = _backgroundColor { return backgroundColor } @@ -41,8 +39,8 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc _backgroundColor = newValue } } - - private var _tintColor: Color? + + open var _tintColor: Color? open var tintColor: Color { get { if let tintColor = _tintColor { return tintColor } @@ -54,7 +52,6 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc _tintColor = newValue } } - /// If true, we add the button in the backButton property. If false we do not add the button. If nil, we add the button if the controller is not the bottom of the stack open var alwaysShowBackButton: Bool? open var backButton: (NavigationButtonModelProtocol & MoleculeModelProtocol)? @@ -62,7 +59,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc open var additionalLeftButtons: [(NavigationButtonModelProtocol & MoleculeModelProtocol)]? open var additionalRightButtons: [(NavigationButtonModelProtocol & MoleculeModelProtocol)]? open var titleView: MoleculeModelProtocol? - open var titleOffset: UIOffset? = UIOffset(horizontal: -CGFloat.greatestFiniteMagnitude, vertical: 0) + open var titleOffset: UIOffset? //-------------------------------------------------- // MARK: - Initializer @@ -100,27 +97,18 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc let typeContainer = try decoder.container(keyedBy: CodingKeys.self) id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString title = try typeContainer.decodeIfPresent(String.self, forKey: .title) - if let hidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidden) { - self.hidden = hidden - } + hidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidden) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) _tintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .tintColor) - if let line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) { - self.line = line - } - if let hidesSystemBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesSystemBackButton) { - self.hidesSystemBackButton = hidesSystemBackButton - } + line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) + hidesSystemBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesSystemBackButton) alwaysShowBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysShowBackButton) backButton = try typeContainer.decodeModelIfPresent(codingKey: .backButton) additionalLeftButtons = try typeContainer.decodeModelsIfPresent(codingKey: .additionalLeftButtons) additionalRightButtons = try typeContainer.decodeModelsIfPresent(codingKey: .additionalRightButtons) titleView = try typeContainer.decodeModelIfPresent(codingKey: .titleView) style = try typeContainer.decodeIfPresent(NavigationItemStyle.self, forKey: .style) - if let titleOffset = try typeContainer.decodeIfPresent(UIOffset.self, forKey: .titleOffset) { - self.titleOffset = titleOffset - } - line?.inverted = style == .dark + titleOffset = try typeContainer.decodeIfPresent(UIOffset.self, forKey: .titleOffset) } open func encode(to encoder: Encoder) throws { @@ -128,11 +116,11 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(title, forKey: .title) - try container.encode(hidden, forKey: .hidden) - try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(hidden, forKey: .hidden) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(_tintColor, forKey: .tintColor) try container.encodeIfPresent(line, forKey: .line) - try container.encode(hidesSystemBackButton, forKey: .hidesSystemBackButton) + try container.encodeIfPresent(hidesSystemBackButton, forKey: .hidesSystemBackButton) try container.encodeIfPresent(alwaysShowBackButton, forKey: .alwaysShowBackButton) try container.encodeModelIfPresent(backButton, forKey: .backButton) try container.encodeModelsIfPresent(additionalLeftButtons, forKey: .additionalLeftButtons) diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/NavigationItemModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/NavigationItemModelProtocol.swift index b9fb111a..3225483f 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/NavigationItemModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/NavigationItemModelProtocol.swift @@ -10,11 +10,11 @@ import Foundation public protocol NavigationItemModelProtocol { var title: String? { get set } - var hidden: Bool { get set } + var hidden: Bool? { get set } var backgroundColor: Color? { get set } var tintColor: Color { get set } var line: LineModel? { get set } - var hidesSystemBackButton: Bool { get set } + var hidesSystemBackButton: Bool? { get set } var alwaysShowBackButton: Bool? { get set } var backButton: (NavigationButtonModelProtocol & MoleculeModelProtocol)? { get set } var additionalLeftButtons: [(NavigationButtonModelProtocol & MoleculeModelProtocol)]? { get set } diff --git a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift index 5a3d9a1a..16200422 100644 --- a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift +++ b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift @@ -23,8 +23,8 @@ public extension UINavigationController { viewController.navigationItem.title = model.title viewController.navigationItem.accessibilityLabel = model.title - viewController.navigationItem.hidesBackButton = model.hidesSystemBackButton - viewController.navigationItem.leftItemsSupplementBackButton = !model.hidesSystemBackButton + viewController.navigationItem.hidesBackButton = model.hidesSystemBackButton ?? true + viewController.navigationItem.leftItemsSupplementBackButton = !viewController.navigationItem.hidesBackButton setNavigationButtons(with: model, for: viewController) setNavigationTitleView(with: model, for: viewController, coordinatingWith: pageBehaviorController) @@ -38,7 +38,7 @@ public extension UINavigationController { func setNavigationButtons(with model: NavigationItemModelProtocol, for viewController: UIViewController) { let delegate = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject var leftItems: [UIBarButtonItem] = [] - if model.hidesSystemBackButton, + if model.hidesSystemBackButton ?? true, model.alwaysShowBackButton != false { if let backButtonModel = model.backButton, NavigationHandler.shared().getViewControllers(for: self).count > 1 || model.alwaysShowBackButton ?? false { @@ -102,7 +102,7 @@ public extension UINavigationController { navigationBar.standardAppearance = appearance navigationBar.scrollEdgeAppearance = appearance - setNavigationBarHidden(model.hidden, animated: false) + setNavigationBarHidden(model.hidden ?? false, animated: false) } @objc @MainActor diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift index 549d53a8..11e33458 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift @@ -119,7 +119,7 @@ public extension MVMCoreUISplitViewController { let delegate = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject // Add back button first. - if navigationItemModel?.hidesSystemBackButton == true { + if navigationItemModel?.hidesSystemBackButton ?? true { var showBackButton: Bool if let forceBackButton = navigationItemModel?.alwaysShowBackButton { showBackButton = forceBackButton From d377ec84b721dd325e9818244b16562b1839b7d7 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Mon, 20 May 2024 21:36:26 -0400 Subject: [PATCH 35/64] Digital PCT265 story PCT-135: Rewire to allow the page transformation behavior to list all changes performed to the model tree. --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + .../BaseControllers/ViewController.swift | 14 +- .../Protocols/PageBehaviorProtocol.swift | 7 +- .../ReplaceableMoleculeBehaviorModel.swift | 25 +- .../JSON/Modelling/UAD_page_model_3.json | 1243 +++++++++++++++++ MVMCoreUITests/MVMCoreUITests.swift | 4 + 6 files changed, 1275 insertions(+), 22 deletions(-) create mode 100644 MVMCoreUITests/JSON/Modelling/UAD_page_model_3.json diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 4893b1d1..191d281f 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335622BF6509C001D90D7 /* UAD_page_model.json */; }; 583335652BF6A5C3001D90D7 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583335642BF6A5C3001D90D7 /* TestUtils.swift */; }; 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */; }; + 5833356D2BFBF51C001D90D7 /* UAD_page_model_3.json in Resources */ = {isa = PBXBuildFile; fileRef = 5833356C2BFBF51C001D90D7 /* UAD_page_model_3.json */; }; 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; }; 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; }; 58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */; }; @@ -797,6 +798,7 @@ 583335622BF6509C001D90D7 /* UAD_page_model.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model.json; sourceTree = ""; }; 583335642BF6A5C3001D90D7 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UAD_page_model_2.json; sourceTree = ""; }; + 5833356C2BFBF51C001D90D7 /* UAD_page_model_3.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model_3.json; sourceTree = ""; }; 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = ""; }; 5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = ""; }; 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = ""; }; @@ -1558,6 +1560,7 @@ children = ( 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */, 583335622BF6509C001D90D7 /* UAD_page_model.json */, + 5833356C2BFBF51C001D90D7 /* UAD_page_model_3.json */, ); path = Modelling; sourceTree = ""; @@ -2740,6 +2743,7 @@ buildActionMask = 2147483647; files = ( 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */, + 5833356D2BFBF51C001D90D7 /* UAD_page_model_3.json in Resources */, 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 50aa1df2..06e3b0f0 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -256,18 +256,14 @@ import MVMCore var behaviorUpdatedModels = [MoleculeModelProtocol]() if var newTemplateModel = newPageModel as? TemplateModelProtocol { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in - if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar) { + var changes = [any MoleculeModelProtocol]() + if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar, changes: &changes) { updatedMolecules.forEach { molecule in + // Replace again in case there is a template level child. if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { // Only recognize the molecules that actually changed. - if let replaced = replaced as? ParentMoleculeModelProtocol, let molecule = molecule as? ParentMoleculeModelProtocol { - let diffs: [MoleculeModelProtocol] = replaced.findAllTheirsNotEqual(against: molecule) - debugLog("Behavior updated \(diffs) in template model.") - behaviorUpdatedModels.append(contentsOf: diffs) - } else if !replaced.isEqual(to: molecule) { - 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.) - } + debugLog("Behavior updated \(changes) in template model.") + behaviorUpdatedModels.append(contentsOf: changes) } else { debugLog("Failed to replace \(molecule) in the template model.") } diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index 6cc30e95..39c72661 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift @@ -31,6 +31,7 @@ public extension PageBehaviorProtocol { public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [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 +42,11 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { public extension PageMoleculeTransformationBehavior { // All optional. - func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { return nil } + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { + var changes = [any MoleculeModelProtocol]() + return onPageNew(rootMolecules: rootMolecules, delegateObject, changes: &changes) + } + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [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) {} diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 698162f6..42f1aea5 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -44,11 +44,10 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co Self.debugLog("Initializing for \((model as! ReplaceableMoleculeBehaviorModel).moleculeIds)") } - public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [MoleculeModelProtocol]? { self.delegateObject = delegateObject - modulesToListenFor = moleculeIds - let moleculeModels = moleculeIds.compactMap { moleculeId in + let moleculeModels = moleculeIds.compactMap { moleculeId in do { return try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId) } catch { @@ -61,23 +60,23 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co } } - return findAndReplace(moleculeModels, in: rootMolecules) + return findAndReplace(moleculeModels, in: rootMolecules, changes: &changes) } - fileprivate func findAndReplace(_ moleculeModels: [any MoleculeModelProtocol], in rootMolecules: [any MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? { + fileprivate func findAndReplace(_ moleculeModels: [any MoleculeModelProtocol], in rootMolecules: [any MoleculeModelProtocol], changes: inout [MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? { debugLog("attempting to replace \(moleculeModels.map { $0.id }) in \(rootMolecules)") - var hasReplacement = false + var changeList = [any MoleculeModelProtocol]() 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("molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...") + debugLog("top molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...") return rootMolecule } - debugLog("replacing \(rootMolecule) with \(updatedMolecule)") + debugLog("top replacing \(rootMolecule) with \(updatedMolecule)") logUpdated(molecule: updatedMolecule) - hasReplacement = true + changeList.append(updatedMolecule) return updatedMolecule } @@ -89,12 +88,12 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co 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("molecule \(newMolecule) is the same as \(replacedMolecule). skipping...") + debugLog("deep molecule \(newMolecule) is the same as \(replacedMolecule). skipping...") return } - debugLog("replacing \(replacedMolecule) with \(newMolecule)") + debugLog("deep replacing \(replacedMolecule) with \(newMolecule)") logUpdated(molecule: newMolecule) - hasReplacement = true + changeList.append(newMolecule) } } catch { let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! @@ -106,6 +105,8 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co } return parentMolecule } + let hasReplacement = !changeList.isEmpty + changes.append(contentsOf: changeList) debugLog("replacing \(hasReplacement ? updatedRootMolecules.count : 0) molecules") return hasReplacement ? updatedRootMolecules : nil } diff --git a/MVMCoreUITests/JSON/Modelling/UAD_page_model_3.json b/MVMCoreUITests/JSON/Modelling/UAD_page_model_3.json new file mode 100644 index 00000000..6d082ac0 --- /dev/null +++ b/MVMCoreUITests/JSON/Modelling/UAD_page_model_3.json @@ -0,0 +1,1243 @@ +{ + "template": "list", + "analyticsData": { + "vzdl.user.customerBusiness": "joint_cust", + "vzdl.target.engagement.intent": "account management", + "vzdl.page.feedCardImpression": "L1|P1|greetingSection|Edit profile & settings^L1|P1|mobileAccountSection|BillsTile^L1|P2|mobileAccountSection|UsageTile^L1|P3|mobileAccountSection|OrdersTile^L1|P4|mobileAccountSection|PlansTile^L1|P5|mobileAccountSection|Services_perksTile^L1|P6|mobileAccountSection|Account_activityTile^L1|P1|fivegHomeAccountSection|BillsTile^L1|P2|fivegHomeAccountSection|OrdersTile^L1|P3|fivegHomeAccountSection|PlansTile^L1|P4|fivegHomeAccountSection|Account_activityTile^L1|P5|fivegHomeAccountSection|Services_perksTile^L1|P1|fiosAccountSection|BillsTile^L1|P2|fiosAccountSection|PlansTile^L1|P3|fiosAccountSection|SupportTile^L1|P4|fiosAccountSection|Account_activityTile^L1|P5|fiosAccountSection|Home_offersTile", + "vzdl.user.id": "a3b00d5af5c7b5d26d017023f12dfa791dba533a33a3ed264c8c98237ae5902f", + "vzdl.user.account": "c0d793b51b000cce8d6ec062a71a224d0faaf8e50c36ebd31240539b31aca001", + "vzdl.user.accountType": "postpaid", + "vzdl.events.uadcardserved": "1", + "vzdl.page.channelSession": "4154e060-85ae-4e8f-a371-fb00c618797a", + "vzdl.page.siteSection": "mva_atomic", + "vzdl.page.sourceChannel": "mva", + "vzdl.page.flow": "account overview", + "vzdl.env.businessUnit": "wireless", + "vzdl.page.displayChannel": "mva", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.id": "atomicAccountLanding" + }, + "molecules": [ + { + "useVerticalMargins": false, + "backgroundColor": "black", + "moleculeName": "tabsListItem", + "tabs": { + "borderLine": false, + "moleculeName": "tabs", + "tabs": [ + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Mobile" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "5G Home" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Fios" + } + } + ], + "style": "dark", + "selectedIndex": 1 + }, + "molecules": [ + [ + { + "analyticsData": { + "vzdl.utils.locationRefId": "500014388|PID506|^500014372|PID505|^500009322|PID1026|", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.feedCardImpression": "L1|P1|quickUpdates|Updates Tile|500014388|LUO_263|10|PID506^L1|P2|quickUpdates||500014372|LUO_262|10|PID505^L1|P3|quickUpdates||500009322|LUO_123|10|PID1026", + "vzwi.mvmapp.pegaFeedSecTwoSession": "6853540150263850930", + "vzdl.events.uadcardserved": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "moleculeName": "listItem", + "ResponseInfo": { + "errorCode": "00", + "topAlertTime": 0, + "type": "Success", + "errorMessage": "SUCCESS" + }, + "id": "priorityTiles", + "gone": false, + "line": { + "type": "none" + }, + "backgroundColor": "black", + "molecule": { + "moleculeName": "carousel", + "height": 168, + "molecules": [ + { + "moleculeName": "carouselItem", + "molecule": { + "axis": "vertical", + "topPadding": 0, + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "molecules": [ + { + "useHorizontalMargins": false, + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularBodyLarge", + "textColor": "#000000", + "moleculeName": "label", + "text": "Make sure important updates reach you. Verify your email is HERMAN.MUNSTER@VERIZON.COM" + }, + "horizontalAlignment": "leading" + } + ], + "axis": "vertical", + "moleculeName": "stack", + "spacing": 0 + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Verify Email ", + "moleculeName": "button", + "enabledTextColor": "#000000", + "action": { + "actions": [ + { + "hideCloseButton": false, + "openOauthWebView": false, + "requestURL": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/gw/udb/mvaFeedback", + "extraParameters": { + "soiEngagementId": "500014388", + "strategyId ": "support/customer service", + "category": "AccountOverview", + "dispositionOptionId": "81", + "propositionName": "LUO_263", + "templateId": "ct_pod6", + "tacticLocation": "Account_Overview", + "dispositionListId": "10", + "locationRefId": "", + "pegaSessionId": "6853540150263850930", + "rank": "2", + "subStrategyId": "Email address not verified", + "tileName": "Updates Tile", + "cardId": "PID506", + "cardWeight": "500", + "propositionId": "LUO_263" + }, + "disableNativeAction": false, + "checkCameraPermission": false, + "showNativeNavigation": false, + "disableAction": false, + "tryToReplaceFirst": false, + "openInWebview": true, + "disableOfflineDevice": false, + "hideUrl": false, + "length": 0, + "actionType": "openPage", + "pageType": "mvaFeedback", + "background": true, + "isSelected": false, + "hideWebNavigation": false, + "location": 0 + }, + { + "appContext": "mobileFirstSS", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|quickUpdates|Updates Tile|500014388|LUO_263|10|PID506", + "vzdl.page.linkName": "PID506_LUO_263|Verify Email ", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "requestFrom": "UAD", + "feedCardClicked": "L1|P1|quickUpdates|Updates Tile|500014388|LUO_263|10|PID506", + "headline_content": "Make sure important updates reach you. Verify your email is HERMAN.MUNSTER@VERIZON.COM", + "uniqueID": "PID506", + "overlay_type": "verify-email" + }, + "actionType": "openPage", + "requestURL": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/gw/udb/verifyEmailMVA", + "pageType": "verifyEmail" + } + ], + "actionType": "actions" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Modify Email", + "moleculeName": "link", + "enabledTextColor": "#000000", + "enabledColor": "#000000", + "action": { + "actions": [ + { + "hideCloseButton": false, + "requestURL": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/gw/udb/mvaFeedback", + "extraParameters": { + "soiEngagementId": "500014388", + "strategyId ": "support/customer service", + "category": "AccountOverview", + "dispositionOptionId": "81", + "propositionName": "LUO_263", + "templateId": "ct_pod6", + "tacticLocation": "Account_Overview", + "dispositionListId": "10", + "locationRefId": "", + "pegaSessionId": "6853540150263850930", + "rank": "2", + "subStrategyId": "Email address not verified", + "tileName": "Updates Tile", + "cardId": "PID506", + "cardWeight": "500", + "propositionId": "LUO_263" + }, + "disableNativeAction": false, + "checkCameraPermission": false, + "showNativeNavigation": false, + "length": 0, + "tryToReplaceFirst": false, + "openInWebview": true, + "disableOfflineDevice": false, + "hideUrl": false, + "actionType": "openPage", + "pageType": "mvaFeedback", + "background": true, + "isSelected": false, + "location": 0, + "hideWebNavigation": false, + "openOauthWebView": false + }, + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|quickUpdates|Updates Tile|500014388|LUO_263|10|PID506", + "vzdl.page.linkName": "PID506_LUO_263", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "requestFrom": "UAD", + "feedCardClicked": "L1|P1|quickUpdates|Updates Tile|500014388|LUO_263|10|PID506", + "headline_content": "Make sure important updates reach you. Verify your email is HERMAN.MUNSTER@VERIZON.COM", + "uniqueID": "PID506", + "overlay_type": "verify-email" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "mngProfile" + } + ], + "actionType": "actions" + } + }, + "verticalAlignment": "center", + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + } + ] + }, + "topPadding": 16, + "useHorizontalMargins": true, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + }, + { + "moleculeName": "carouselItem", + "molecule": { + "axis": "vertical", + "topPadding": 0, + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "molecules": [ + { + "useHorizontalMargins": false, + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularBodyLarge", + "textColor": "#000000", + "moleculeName": "label", + "text": "Let's make sure you get important account updates." + }, + "horizontalAlignment": "leading" + } + ], + "axis": "vertical", + "moleculeName": "stack", + "spacing": 0 + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Add email", + "moleculeName": "button", + "enabledTextColor": "#000000", + "action": { + "actions": [ + { + "hideCloseButton": false, + "requestURL": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/gw/udb/mvaFeedback", + "extraParameters": { + "soiEngagementId": "500014372", + "strategyId ": "", + "category": "AccountOverview", + "dispositionOptionId": "81", + "propositionName": "LUO_262", + "templateId": "", + "tacticLocation": "Account_Overview", + "dispositionListId": "10", + "locationRefId": "", + "pegaSessionId": "6853540150263850930", + "rank": "3", + "subStrategyId": "Missing SubStrategyid", + "tileName": "", + "cardId": "PID505", + "cardWeight": "500", + "propositionId": "LUO_262" + }, + "disableNativeAction": false, + "checkCameraPermission": false, + "showNativeNavigation": false, + "length": 0, + "tryToReplaceFirst": false, + "openInWebview": true, + "disableOfflineDevice": false, + "hideUrl": false, + "actionType": "openPage", + "pageType": "mvaFeedback", + "background": true, + "isSelected": false, + "location": 0, + "hideWebNavigation": false, + "openOauthWebView": false + }, + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|quickUpdates||500014372|LUO_262|10|PID505", + "vzdl.page.linkName": "PID505_LUO_262|Add email", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "mngProfile" + } + ], + "actionType": "actions" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + } + ] + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + } + ] + }, + "topPadding": 16, + "useHorizontalMargins": true, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + }, + { + "moleculeName": "carouselItem", + "molecule": { + "axis": "vertical", + "topPadding": 0, + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "molecules": [ + { + "useHorizontalMargins": false, + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularBodyLarge", + "textColor": "#000000", + "moleculeName": "label", + "text": "Take a peek at next month’s bill." + }, + "horizontalAlignment": "leading" + } + ], + "axis": "vertical", + "moleculeName": "stack", + "spacing": 0 + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Review details", + "moleculeName": "button", + "enabledTextColor": "#000000", + "action": { + "actions": [ + { + "hideCloseButton": false, + "requestURL": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/gw/udb/mvaFeedback", + "extraParameters": { + "soiEngagementId": "500009322", + "strategyId ": "Bill", + "category": "AccountOverview", + "dispositionOptionId": "81", + "propositionName": "LUO_123", + "templateId": "", + "tacticLocation": "Account_Overview", + "dispositionListId": "10", + "locationRefId": "", + "pegaSessionId": "6853540150263850930", + "rank": "4", + "subStrategyId": "Next bill estimate", + "tileName": "", + "cardId": "PID1026", + "cardWeight": "300", + "propositionId": "LUO_123" + }, + "disableNativeAction": false, + "checkCameraPermission": false, + "showNativeNavigation": false, + "length": 0, + "tryToReplaceFirst": false, + "openInWebview": true, + "disableOfflineDevice": false, + "hideUrl": false, + "actionType": "openPage", + "pageType": "mvaFeedback", + "background": true, + "isSelected": false, + "location": 0, + "hideWebNavigation": false, + "openOauthWebView": false + }, + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|quickUpdates||500009322|LUO_123|10|PID1026", + "vzdl.page.linkName": "PID1026_LUO_123|Review details", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa3.verizonwireless.com/digital/nsa/secure/ui/bill/nbs/#/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + } + ] + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "leading" + } + ] + }, + "topPadding": 16, + "useHorizontalMargins": true, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + } + ], + "itemWidthPercent": 72, + "accessibilityText": "carousel", + "spacing": 12, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20, + "inverted": true + }, + "rightPadding": 16, + "leftPadding": 16 + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Bills", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|mobileAccountSection|BillsTile", + "vzdl.page.linkName": "Mobile|Bills", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/bill/overview/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Usage", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|mobileAccountSection|UsageTile", + "vzdl.page.linkName": "Mobile|Usage", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "sourcePage": "plansAndDeviceLanding", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "dataUsageDetails" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Orders", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|mobileAccountSection|OrdersTile", + "vzdl.page.linkName": "Mobile|Orders", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/orders/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Plans", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P4|mobileAccountSection|PlansTile", + "vzdl.page.linkName": "Mobile|Plans", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/cpc/mvm?pmd=y&tabbar=true", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Services & perks", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P5|mobileAccountSection|Services_perksTile", + "vzdl.page.linkName": "Mobile|Services & perks", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/products/producthub/home", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "id": "notification_count", + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Account activity", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P6|mobileAccountSection|Account_activityTile", + "vzdl.page.linkName": "Mobile|Account activity", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/optg/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "moleculeName": "listItem", + "line": { + "moleculeName": "line", + "type": "none" + }, + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Devices", + "fontStyle": "BoldTitleMedium" + }, + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Manage all devices", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/devices/landing/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Manage all devices" + }, + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "moleculeName": "listItem", + "style": "none", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "moleculeName": "carouselItem", + "molecule": { + "molecule": { + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Add a line and get select phones on us", + "fontStyle": "RegularMicro" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularMicro", + "numberOfLines": 1, + "moleculeName": "label", + "text": "\n\n\n\n\n" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Shop", + "moleculeName": "button", + "action": { + "extraParameters": { + "browserUrl": "sales/next/shop.html?flow=AAL&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "center" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Bring my own", + "moleculeName": "link", + "action": { + "extraParameters": { + "browserUrl": "sales/digital/byod.html?flow=NSO&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "fill", + "topPadding": 12, + "image": { + "image": "https://ss7.vzw.com/is/image/VerizonWireless/background-image-mobile?&scl=2", + "moleculeName": "image" + }, + "moleculeName": "bgImageContainer", + "bottomPadding": 12 + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 48, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "label", + "text": "Make the most of your plans", + "fontStyle": "BoldTitleMedium" + } + }, + { + "id": "forYouTiles", + "style": "none", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Discover more", + "fontStyle": "BoldTitleMedium" + }, + "verticalAlignment": "center", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Mobile|Shop all", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "locale": "EN", + "browserUrl": "sales/digital/shoplanding.html?isShopFlow=true&entrypoint=tabbar", + "requestFrom": "Shop" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Shop all" + }, + "verticalAlignment": "center", + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "id": "discoverTiles", + "style": "none", + "moleculeName": "listItem", + "bottomPadding": 32, + "useVerticalMargins": true, + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + } + ] + ] + } + ], + "backgroundColor": "black", + "hab": { + "configuration": "single", + "inverted": false + }, + "cache": true, + "header": { + "useVerticalMargins": true, + "backgroundColor": "black", + "moleculeName": "stack", + "bottomPadding": 12, + "molecules": [ + { + "moleculeName": "stackItem", + "id": "greetingSection", + "molecule": { + "fontStyle": "RegularTitleMedium", + "textColor": "#FFFFFF", + "moleculeName": "label", + "text": "Hi Lebowski." + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "enabledColor": "white", + "title": "Edit profile & settings", + "moleculeName": "link", + "inverted": true, + "action": { + "actionType": "openPage", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|greetingSection|Edit profile & settings", + "vzdl.page.linkName": "Edit profile & settings", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "pageType": "oneVzIdSettingsLanding", + "presentationStyle": "push", + "requestURL": "https://mobile-exp-qa2.vzw.com/mobile/nsa/nos/gw/launchapp/l2/webview", + "tryToReplaceFirst": false, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/acct/unifiedprofile/overview" + } + } + } + } + ] + }, + "behaviors": [ + { + "moleculeIds": [ + "priorityTiles", + "forYouTiles", + "discoverTiles", + "priorityTiles5G", + "forYouTiles5G", + "discoverTiles5G", + "notification_count", + "priorityTilesFIOS", + "phoneServiceFIOS", + "internetServiceFIOS", + "tvServiceFIOS", + "forYouTilesFIOS", + "discoverTilesFIOS", + "FiosBills", + "FiosSubBills", + "FiosPlans", + "FiosSupport", + "notification_count_fios", + "greetingSection", + "FiosHomeOffers", + "priorityTilesLTE", + "forYouTilesLTE", + "discoverTilesLTE" + ], + "behaviorName": "replaceMoleculeBehavior" + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/topCardsMVA", + "pageType": "topCardsMVA" + } + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/bottomCardsMVA", + "pageType": "bottomCardsMVA" + } + } + ], + "pageType": "atomicAccountLanding", + "loggedInMdn": "2815434851", + "presentationStyle": "root", + "footerlessSpacerColor": "white", + "tabBarIndex": 1, + "navigationBar": { + "style": "dark", + "moleculeName": "navigationBar", + "additionalLeftButtons": [ + { + "accessibilityText": "Verizon logo button, tap anytime to scroll to top of page", + "moleculeName": "navigationImageButton", + "image": "nav_vz_mark", + "imageRenderingMode": "alwaysOriginal", + "action": { + "actionType": "noop" + } + } + ], + "additionalRightButtons": [ + { + "accessibilityText": "Stores", + "moleculeName": "navigationImageButton", + "image": "nav_stores_white", + "action": { + "analyticsData": { + "vzdl.page.linkName": "global tab nav:stores" + }, + "title": "Stores", + "actionType": "openPage", + "pageType": "rtlStoreJourney" + } + } + ] + }, + "footerlessSpacerHeight": 0 +} + diff --git a/MVMCoreUITests/MVMCoreUITests.swift b/MVMCoreUITests/MVMCoreUITests.swift index af631c93..5cbcb62a 100644 --- a/MVMCoreUITests/MVMCoreUITests.swift +++ b/MVMCoreUITests/MVMCoreUITests.swift @@ -189,6 +189,7 @@ final class MVMCoreUITests: XCTestCase { let listTemplateModel1 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) let listTemplateModel2 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) let listTemplateModel3 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_2")) + let listTemplateModel4 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_3")) let results = listTemplateModel1.findFirst(in: listTemplateModel2, failing: { $0.isEqual(to: $1) }) XCTAssertFalse(results.matched) @@ -199,6 +200,9 @@ final class MVMCoreUITests: XCTestCase { XCTAssertTrue(results2.matched) XCTAssertFalse(listTemplateModel1.deepEquals(to: listTemplateModel3)) XCTAssertTrue(listTemplateModel1.isDeeplyVisuallyEquivalent(to: listTemplateModel3)) + + let results3: [MoleculeModelProtocol] = listTemplateModel1.findAllTheirsNotEqual(against: listTemplateModel4) + XCTAssertTrue(results3.count == 2) } func testPageEqualityPerformance() throws { From 75104dbe4d31dfd9ca21db006dfd4bfb0552426a Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Mon, 20 May 2024 23:16:16 -0400 Subject: [PATCH 36/64] Digital PCT265 story PCT-135: Revert onPageNew signature change. --- MVMCoreUI/Accessibility/AccessibilityHandler.swift | 3 +-- MVMCoreUI/BaseControllers/ViewController.swift | 3 +++ .../Behaviors/Protocols/PageBehaviorProtocol.swift | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/MVMCoreUI/Accessibility/AccessibilityHandler.swift b/MVMCoreUI/Accessibility/AccessibilityHandler.swift index 76e556e6..65ae8272 100644 --- a/MVMCoreUI/Accessibility/AccessibilityHandler.swift +++ b/MVMCoreUI/Accessibility/AccessibilityHandler.swift @@ -261,9 +261,8 @@ open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTra accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method. } - open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { + open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject) - return nil } open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) { diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 06e3b0f0..1579a790 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -263,6 +263,9 @@ import MVMCore if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { // Only recognize the molecules that actually changed. debugLog("Behavior updated \(changes) in template model.") + changes = changes.filter({ model in + behaviorUpdatedModels.contains { $0.id == model.id } + }) behaviorUpdatedModels.append(contentsOf: changes) } else { debugLog("Failed to replace \(molecule) in the template model.") diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index 39c72661..9fa84f17 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift @@ -30,7 +30,7 @@ public extension PageBehaviorProtocol { */ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { - func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [MoleculeModelProtocol]? func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?) func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) @@ -42,11 +42,11 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { public extension PageMoleculeTransformationBehavior { // All optional. - func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) -> [MoleculeModelProtocol]? { - var changes = [any MoleculeModelProtocol]() - return onPageNew(rootMolecules: rootMolecules, delegateObject, changes: &changes) + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {} + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [MoleculeModelProtocol]? { + onPageNew(rootMolecules: rootMolecules, delegateObject) // Call the original signature. + return nil // Don't return any tranformations. } - func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [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) {} From 3ed40e52bed3ffeb1496815f63a2018a32af0b5b Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 13:56:02 -0400 Subject: [PATCH 37/64] Digital PCT265 story PCT-135: Deep equals check for replacement changes. --- .../ModelProtocols/MoleculeModelProtocol.swift | 2 +- .../ModelProtocols/ParentMoleculeModelProtocol.swift | 10 ++++++++++ .../Behaviors/ReplaceableMoleculeBehaviorModel.swift | 6 +++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index 3dc09d7c..672105ef 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -19,7 +19,7 @@ public extension MoleculeModelProtocol { static var categoryCodingKey: String { "moleculeName" } - func isEqual(to model: ModelProtocol) -> Bool { + func isEqual(to model: ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return moleculeName == model.moleculeName && backgroundColor == model.backgroundColor diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 88b09b86..d0e1c13f 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -181,3 +181,13 @@ public extension ParentModelProtocol { return allDiffs } } + +public extension ModelComparisonProtocol where Self: MoleculeModelProtocol { + + func deepEquals(to model: ModelProtocol) -> Bool { + if let self = self as? ParentModelProtocol { + return self.deepEquals(to: model) + } + return isEqual(to: model) + } +} diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 42f1aea5..c03594d4 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -70,7 +70,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co // 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 { + guard !updatedMolecule.deepEquals(to: rootMolecule) else { debugLog("top molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...") return rootMolecule } @@ -86,7 +86,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co moleculeModels.forEach { newMolecule in do { if let replacedMolecule = try parentMolecule.replaceChildMolecule(with: newMolecule) { - guard !replacedMolecule.isEqual(to: newMolecule) else { + 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...") return @@ -107,7 +107,7 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co } let hasReplacement = !changeList.isEmpty changes.append(contentsOf: changeList) - debugLog("replacing \(hasReplacement ? updatedRootMolecules.count : 0) molecules") + debugLog("replacing \(hasReplacement ? updatedRootMolecules.count : 0) molecules for \(changes)") return hasReplacement ? updatedRootMolecules : nil } From 8135a126ed39cdd9e576fca7762641b480bc04f0 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 13:57:04 -0400 Subject: [PATCH 38/64] Digital PCT265 story PCT-135: Remove subnav from listening to page updates. --- MVMCoreUI/Managers/SubNav/SubNavManagerController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift index 2bde433a..8a768545 100644 --- a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift +++ b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift @@ -348,6 +348,10 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, [tabs] } + open override func observeForResponseJSONUpdates() { + // Don't observe for updates. Children will update through newDataReceived. + } + open func newDataReceived(in viewController: UIViewController) { manager?.newDataReceived?(in: viewController) hideNavigationBarLine(true) From f403e34d7d98136a8c98171f5c73514efb940cae Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 14:00:07 -0400 Subject: [PATCH 39/64] Digital PCT265 story PCT-135: Change filtering fix, disable partial page updates, move manager newDataReceived. --- .../BaseControllers/ViewController.swift | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 1579a790..928a9390 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -105,10 +105,6 @@ import MVMCore 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) @@ -262,11 +258,13 @@ import MVMCore // Replace again in case there is a template level child. if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { // Only recognize the molecules that actually changed. - debugLog("Behavior updated \(changes) in template model.") - changes = changes.filter({ model in - behaviorUpdatedModels.contains { $0.id == model.id } - }) - behaviorUpdatedModels.append(contentsOf: changes) + if changes.count > 0 { + debugLog("\(behavior) updated \(changes) in template model.") + changes = changes.filter({ model in + !behaviorUpdatedModels.contains { $0.id == model.id } + }) + behaviorUpdatedModels.append(contentsOf: changes) + } } else { debugLog("Failed to replace \(molecule) in the template model.") } @@ -289,37 +287,36 @@ import MVMCore 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.theirs as? MoleculeModelProtocol } + // This isn't ready yet. handleNewData for the ListTemplate triggers a row item reset and full rebuild + StackTemplate isn't targeting individual refreshes anyway. +// pageUpdatedModels = originalModel.findAllTheirsNotEqual(against: newPageModel) +// debugLog("Page molecule updates\n\(pageUpdatedModels)") + isFirstRender = true // Instead force a full render whenever there is a page data change. } - let allUpdatedMolecules = isFirstRender ? [] : behaviorUpdatedModels + pageUpdatedModels + let allUpdatedMolecules = behaviorUpdatedModels //+ pageUpdatedModels - isFirstRender = false + // 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) // Dispatch to decouple execution. First massage data through template classes, then render. Task { @MainActor in - if allUpdatedMolecules.isEmpty { + if allUpdatedMolecules.isEmpty || isFirstRender { debugLog("Performing full page render...") updateUI() } else { debugLog("Performing partial render of \(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. open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + isFirstRender = false + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.willRender(rootMolecules: molecules ?? getRootMolecules(), delegateObjectIVar) } From 042cc208591f3d60f9ff078ccc0df4af0791cb71 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 17:00:39 -0400 Subject: [PATCH 40/64] Digital PCT265 story PCT-135: Cleanup. --- MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 01be79bd..2f2099f6 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -30,9 +30,6 @@ import UIKit } public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { -// if let listModel = model as? ListItemModel, listModel.hasStableId { -// return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" -// } return MoleculeContainer.nameForReuse(with: model, delegateObject) } From d79ec8dc6a81a1a2f5a821205b1242983546851c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 17:11:15 -0400 Subject: [PATCH 41/64] Digital PCT265 story PCT-135: Extract running the behavior transformations for method clarity. --- .../BaseControllers/ViewController.swift | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 928a9390..7977ddc1 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -251,26 +251,7 @@ import MVMCore // Run through behavior tranformations. var behaviorUpdatedModels = [MoleculeModelProtocol]() if var newTemplateModel = newPageModel as? TemplateModelProtocol { - executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in - var changes = [any MoleculeModelProtocol]() - if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar, changes: &changes) { - updatedMolecules.forEach { molecule in - // Replace again in case there is a template level child. - if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { - // Only recognize the molecules that actually changed. - if changes.count > 0 { - debugLog("\(behavior) updated \(changes) in template model.") - changes = changes.filter({ model in - !behaviorUpdatedModels.contains { $0.id == model.id } - }) - behaviorUpdatedModels.append(contentsOf: changes) - } - } else { - debugLog("Failed to replace \(molecule) in the template model.") - } - } - } - } + behaviorUpdatedModels = runBehaviorTransformations(on: &newTemplateModel) } // Apply the form validator to the controller. @@ -331,6 +312,31 @@ import MVMCore view.setNeedsLayout() } + func runBehaviorTransformations(on newTemplateModel: inout any TemplateModelProtocol) -> [any MoleculeModelProtocol] { + var behaviorUpdatedModels = [MoleculeModelProtocol]() + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + var changes = [any MoleculeModelProtocol]() + if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar, changes: &changes) { + updatedMolecules.forEach { molecule in + // Replace again in case there is a template level child. + if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { + // Only recognize the molecules that actually changed. + if changes.count > 0 { + debugLog("\(behavior) updated \(changes) in template model.") + changes = changes.filter({ model in + !behaviorUpdatedModels.contains { $0.id == model.id } + }) + behaviorUpdatedModels.append(contentsOf: changes) + } + } else { + debugLog("Failed to replace \(molecule) in the template model.") + } + } + } + } + return behaviorUpdatedModels + } + public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in behavior.willSetupMolecule(with: model, updating: nil) From 6421c53f75cc2b304d8dd4f4033fa4da71ed7dd6 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 21 May 2024 20:28:51 -0400 Subject: [PATCH 42/64] Digital PCT265 story PCT-135: Resovle merge conflicts & updates. --- .../Atomic/Molecules/NavigationBar/NavigationItemModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 807d0e89..2c6a8d7d 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -130,7 +130,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc try container.encodeIfPresent(titleOffset, forKey: .titleOffset) } - public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && title == model.title From f940026bf174ec9ec95d2d71bed2190f09d0a3ed Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 22 May 2024 09:36:20 -0400 Subject: [PATCH 43/64] Digital ACT191 defect ONEAPP-7459 - Fix missing title adjustment commit --- .../NavigationController/UINavigationController+Extension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift index 16200422..426aa812 100644 --- a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift +++ b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift @@ -97,7 +97,7 @@ public extension UINavigationController { NSAttributedString.Key.foregroundColor: tint]; appearance.backgroundColor = backgroundColor appearance.titleTextAttributes.updateValue(tint, forKey: .foregroundColor) - appearance.titlePositionAdjustment = model.titleOffset ?? .zero + appearance.titlePositionAdjustment = model.titleOffset ?? UIOffset(horizontal: -CGFloat.greatestFiniteMagnitude, vertical: 0) appearance.setShadow(for: model.line) navigationBar.standardAppearance = appearance navigationBar.scrollEdgeAppearance = appearance From 9ff5b58eb5aa57798d21f3e401faf7ed38c8ba46 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 22 May 2024 18:00:17 -0400 Subject: [PATCH 44/64] Digital PCT265 story PCT-135: Carousel item clips to bounds restore. --- MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift index 509c9e61..9e9e0bc2 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift @@ -22,8 +22,8 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { open override func setupView() { super.setupView() - clipsToBounds = false - + clipsToBounds = true // Needed for container view corner rounding of subviews. + // Covers the card when peaking. peakingCover.backgroundColor = .white peakingCover.alpha = 0 From cb659b63e9898fb01cf4f183edf94a87f0fe683b Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 22 May 2024 18:00:59 -0400 Subject: [PATCH 45/64] Digital PCT265 story PCT-135: ContainerModel ModelComparisonProtocol conformance. --- .../Molecules/Items/ListItemModel.swift | 6 +++--- .../Molecules/Items/StackItemModel.swift | 2 +- .../MoleculeContainerModel.swift | 8 ++++---- MVMCoreUI/Atomic/Organisms/StackModel.swift | 2 +- .../Containers/Views/ContainerModel.swift | 19 ++++++++++++++++--- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index 0128c41e..3ac37b49 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -8,7 +8,7 @@ // A base class that has common list item boilerplate model stuffs. import MVMCore -@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol, ModelComparisonProtocol, MoleculeModelComparisonProtocol { +@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol, MoleculeModelComparisonProtocol { //-------------------------------------------------- // MARK: - Properties @@ -131,8 +131,8 @@ import MVMCore try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } - public func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && hideArrow == model.hideArrow && style == model.style diff --git a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift index 85c4cf86..edb31463 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/StackItemModel.swift @@ -37,7 +37,7 @@ fatalError("init(from:) has not been implemented") } - public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && spacing == model.spacing diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index a97f4af6..140435ad 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -62,13 +62,13 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - public func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } return backgroundColor == model.backgroundColor } + // Declare for overrides. public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor + return isEqual(to: model) } } diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 3b687c18..bffe460b 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -79,7 +79,7 @@ try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && axis == model.axis diff --git a/MVMCoreUI/Containers/Views/ContainerModel.swift b/MVMCoreUI/Containers/Views/ContainerModel.swift index 81b7c769..b9549bf8 100644 --- a/MVMCoreUI/Containers/Views/ContainerModel.swift +++ b/MVMCoreUI/Containers/Views/ContainerModel.swift @@ -7,7 +7,7 @@ // -open class ContainerModel: ContainerModelProtocol, Codable { +open class ContainerModel: ContainerModelProtocol, Codable, ModelComparisonProtocol { //-------------------------------------------------- // MARK: - Properties @@ -75,7 +75,7 @@ open class ContainerModel: ContainerModelProtocol, Codable { //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- - + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -99,7 +99,7 @@ open class ContainerModel: ContainerModelProtocol, Codable { cornerRadius = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .cornerRadius) setDefaults() } - + open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) @@ -113,4 +113,17 @@ open class ContainerModel: ContainerModelProtocol, Codable { try container.encodeIfPresent(bottomPadding, forKey: .bottomPadding) try container.encodeIfPresent(cornerRadius, forKey: .cornerRadius) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return horizontalAlignment == model.horizontalAlignment + && useHorizontalMargins == model.useHorizontalMargins + && leftPadding == model.leftPadding + && rightPadding == model.rightPadding + && verticalAlignment == model.verticalAlignment + && useVerticalMargins == model.useVerticalMargins + && topPadding == model.topPadding + && bottomPadding == model.bottomPadding + && cornerRadius == model.cornerRadius + } } From 7956a643cb1fe5e4beebe26052da950a6ab61fe7 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Thu, 23 May 2024 12:00:28 -0400 Subject: [PATCH 46/64] Digital ACT191 defect CXTDT-561854 - Update font of navigation bar label button to BoldBodySmall --- .../Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift index 1eb4c8a1..5a99d670 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift @@ -16,7 +16,7 @@ let actionObject = ActionDelegate() let button = self.init(title: title, style: .plain, target: actionObject, action: #selector(actionObject.callActionBlock(_:))) button.actionDelegate = actionObject - button.setTitleTextAttributes([NSAttributedString.Key.font: Styler.Font.RegularBodySmall.getFont()], for: .normal) + button.setTitleTextAttributes([NSAttributedString.Key.font: Styler.Font.BoldBodySmall.getFont()], for: .normal) return button } From 97d7e1dba1d7bc3cb3e68f85bb0916b11c069d27 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 23 May 2024 11:18:23 -0500 Subject: [PATCH 47/64] fixed issue with decoding Signed-off-by: Matt Bruce --- .../Extensions/VDS-Tilelet+Codable.swift | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift index 17bca4b5..b0a0c46e 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift @@ -35,41 +35,67 @@ extension Tilelet.BadgeModel: Codable { extension Tilelet.DescriptiveIcon: Codable { private enum CodingKeys: String, CodingKey { - case name, size, surface + case name, size, color, accessibleText } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let name = try container.decode(Icon.Name.self, forKey: .name) let size = try container.decodeIfPresent(Icon.Size.self, forKey: .size) ?? .medium - let surface = try container.decodeIfPresent(Surface.self, forKey: .surface) ?? .dark - self.init(name: name, size: size, surface: surface) + let color = try container.decodeIfPresent(Color.self, forKey: .color) + let accessibleText = try? container.decode(String.self, forKey: .accessibleText) + if let uiColor = color?.uiColor { + self.init(name: name, + colorConfiguration: .init(uiColor, uiColor), + size: size, + accessibleText: accessibleText) + } else { + self.init(name: name, + size: size, + accessibleText: accessibleText) + } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(size, forKey: .size) - try container.encode(surface, forKey: .surface) + try container.encode(accessibleText, forKey: .accessibleText) + try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) } } +extension Tilelet.DirectionalIcon.IconType : Codable {} +extension Tilelet.DirectionalIcon.IconSize : Codable {} + extension Tilelet.DirectionalIcon: Codable { private enum CodingKeys: String, CodingKey { - case size, surface + case name, size, color, accessibleText } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let size = try container.decodeIfPresent(Icon.Size.self, forKey: .size) ?? .medium - let surface = try container.decodeIfPresent(Surface.self, forKey: .surface) ?? .dark - self.init(size: size, surface: surface) + let iconType = try container.decodeIfPresent(IconType.self, forKey: .name) ?? .rightArrow + let size = try container.decodeIfPresent(IconSize.self, forKey: .size) ?? .medium + let color = try container.decodeIfPresent(Color.self, forKey: .color) + let accessibleText = try? container.decode(String.self, forKey: .accessibleText) + if let uiColor = color?.uiColor { + self.init(iconType: iconType, + colorConfiguration: .init(uiColor, uiColor), + size: size, + accessibleText: accessibleText) + } else { + self.init(iconType: iconType, + size: size, + accessibleText: accessibleText) + } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(iconType, forKey: .name) try container.encode(size, forKey: .size) - try container.encode(surface, forKey: .surface) + try container.encode(accessibleText, forKey: .accessibleText) + try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) } } - From 062a1cfe64406e4f426e7489f217ceb7de56cb09 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 23 May 2024 11:24:39 -0500 Subject: [PATCH 48/64] accessibleText not required Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift index b0a0c46e..76e89290 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift @@ -43,7 +43,7 @@ extension Tilelet.DescriptiveIcon: Codable { let name = try container.decode(Icon.Name.self, forKey: .name) let size = try container.decodeIfPresent(Icon.Size.self, forKey: .size) ?? .medium let color = try container.decodeIfPresent(Color.self, forKey: .color) - let accessibleText = try? container.decode(String.self, forKey: .accessibleText) + let accessibleText = try container.decodeIfPresent(String.self, forKey: .accessibleText) if let uiColor = color?.uiColor { self.init(name: name, colorConfiguration: .init(uiColor, uiColor), @@ -78,7 +78,7 @@ extension Tilelet.DirectionalIcon: Codable { let iconType = try container.decodeIfPresent(IconType.self, forKey: .name) ?? .rightArrow let size = try container.decodeIfPresent(IconSize.self, forKey: .size) ?? .medium let color = try container.decodeIfPresent(Color.self, forKey: .color) - let accessibleText = try? container.decode(String.self, forKey: .accessibleText) + let accessibleText = try container.decodeIfPresent(String.self, forKey: .accessibleText) if let uiColor = color?.uiColor { self.init(iconType: iconType, colorConfiguration: .init(uiColor, uiColor), From d8f642258f638fa531fb83559b72a0d6ad56e623 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 23 May 2024 11:25:52 -0500 Subject: [PATCH 49/64] encodeIfPresent Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift index 76e89290..9d874675 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift @@ -60,7 +60,7 @@ extension Tilelet.DescriptiveIcon: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(size, forKey: .size) - try container.encode(accessibleText, forKey: .accessibleText) + try container.encodeIfPresent(accessibleText, forKey: .accessibleText) try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) } } @@ -95,7 +95,7 @@ extension Tilelet.DirectionalIcon: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(iconType, forKey: .name) try container.encode(size, forKey: .size) - try container.encode(accessibleText, forKey: .accessibleText) + try container.encodeIfPresent(accessibleText, forKey: .accessibleText) try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) } } From 54c697dd9d165f3d024075cbf728d27de6a7e232 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Thu, 23 May 2024 16:18:46 -0400 Subject: [PATCH 50/64] Digital PCT265 story PCT-135: Carousel item clipping VDS conditional approach. --- MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift | 4 ++++ MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift | 10 ++++++++++ MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift | 5 ++++- .../OtherHandlers/MVMCoreUIViewConstrainingProtocol.h | 2 ++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift index e00c2784..6296f640 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift @@ -136,4 +136,8 @@ open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{ extension TileContainer: MVMCoreUIViewConstrainingProtocol { public func horizontalAlignment() -> UIStackView.Alignment { .leading } + + public func isClippable() -> Bool { + return false + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift index 58a91de5..8ed4fd55 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift @@ -134,3 +134,13 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{ } } } + +extension Tilelet: MVMCoreUIViewConstrainingProtocol { + + // Investigate later. + //public func horizontalAlignment() -> UIStackView.Alignment { .leading } + + public func isClippable() -> Bool { + return false + } +} diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift index 9e9e0bc2..5350e71c 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift @@ -7,6 +7,7 @@ // import Foundation +import VDS open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { @@ -17,12 +18,14 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { open override func addMolecule(_ molecule: MoleculeViewProtocol) { super.addMolecule(molecule) + + clipsToBounds = (molecule as? MVMCoreUIViewConstrainingProtocol)?.isClippable?() ?? true + contentView.sendSubviewToBack(molecule) } open override func setupView() { super.setupView() - clipsToBounds = true // Needed for container view corner rounding of subviews. // Covers the card when peaking. peakingCover.backgroundColor = .white diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h b/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h index 12aaacdb..c4680824 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h @@ -37,4 +37,6 @@ /// Containing Views can tell the contained if they should use vertical margins. - (void)shouldSetVerticalMargins:(BOOL)shouldSet; +- (BOOL)isClippable; + @end From 6d8f8bd29832c1c0c8a55dcaca4abcab52eeaaa0 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 17:25:43 -0400 Subject: [PATCH 51/64] Digital PCT265 story PCT-135: Code review fix in AlertModel equality check. --- MVMCoreUI/Atomic/Actions/AlertModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Actions/AlertModel.swift b/MVMCoreUI/Atomic/Actions/AlertModel.swift index 683d87a4..ea2f171c 100644 --- a/MVMCoreUI/Atomic/Actions/AlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/AlertModel.swift @@ -159,7 +159,7 @@ public struct AlertModel: Codable, Identifiable, Equatable, AlertModelProtocol { public static func == (lhs: AlertModel, rhs: AlertModel) -> Bool { lhs.title == rhs.title - && lhs.message == rhs.title + && lhs.message == rhs.message && lhs.preferredStyle == rhs.preferredStyle && lhs.buttonModels == rhs.buttonModels && lhs.analyticsData == rhs.analyticsData From 5660a3273f1e10cfeac56f281fc8b6a7abaab1c1 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 17:28:51 -0400 Subject: [PATCH 52/64] Digital PCT265 story PCT-135: Code review of headerH2TwoRows double body2 replace check. --- .../Headers/H2/HeadersH2PricingTwoRowsModel.swift | 1 - .../Protocols/ModelProtocols/MoleculeComparisonProtocol.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift index b92e04d1..1205c55c 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift @@ -31,7 +31,6 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &subBody, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &body2, with: molecule, replaced: &replacedMolecule) - || replaceChildMolecule(at: &body2, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &subBody2, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &body3, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &subBody3, with: molecule, replaced: &replacedMolecule) { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 5e47e33f..04a3a4bf 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -37,7 +37,7 @@ public extension MoleculeModelComparisonProtocol where Self: ParentModelProtocol public extension Optional { - /// Checks if the curent model is equal to another model. + /// Checks if the current model is equal to another model. func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol?) -> Bool { guard let self = self as? MoleculeModelComparisonProtocol else { return model == nil From 79972fc2624e6471ac44c31974ccced4266c5e7f Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 17:33:34 -0400 Subject: [PATCH 53/64] Digital PCT265 story PCT-135: Code review. Missing properties in TitleLockup equality check. --- .../DesignedComponents/LockUps/TitleLockupModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index e757fb3b..3112cc2e 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -49,10 +49,13 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return textAlignment == model.textAlignment - && subTitleColor == model.subTitleColor && alignment == model.alignment + && titleColor == model.titleColor + && subTitleColor == model.subTitleColor && inverted == model.inverted && backgroundColor == model.backgroundColor + && eyebrow.matchExistence(with: model.eyebrow) + && subTitle.matchExistence(with: model.subTitle) } //-------------------------------------------------- From 9fdc5c5b699f81e20231709c61d262d90d623e0c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 17:39:09 -0400 Subject: [PATCH 54/64] Digital PCT265 story PCT-135: Code reivew. Missing unSelectedColor check in TabBarModel. --- .../Molecules/HorizontalCombinationViews/TabBarModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift index 03bd6dc8..b16ff288 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -112,6 +112,7 @@ open class TabBarModel: MoleculeModelProtocol { return backgroundColor == model.backgroundColor && selectedColor == model.selectedColor && selectedTab == model.selectedTab + && unSelectedColor == model.unSelectedColor && style == model.style && tabs == model.tabs } @@ -121,6 +122,7 @@ open class TabBarModel: MoleculeModelProtocol { return backgroundColor == model.backgroundColor && selectedColor == model.selectedColor && selectedTab == model.selectedTab + && unSelectedColor == model.unSelectedColor && style == model.style && tabs.isVisuallyEquivalent(to: model.tabs) } From b3d1bd02b0957bb9d969a3b56dc658248be0f433 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 17:59:34 -0400 Subject: [PATCH 55/64] Digital PCT265 story PCT-135: Code reivew. NavigationItemModel missing property checks. --- .../Molecules/NavigationBar/NavigationItemModel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 2c6a8d7d..18aa904e 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -139,6 +139,13 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc && line.isEqual(to: model.line) && hidesSystemBackButton == model.hidesSystemBackButton && style == model.style + && titleOffset == model.titleOffset + && titleView.isEqual(to: model.titleView) + && additionalLeftButtons.isEqual(to: model.additionalLeftButtons) + && additionalRightButtons.isEqual(to: model.additionalRightButtons) + && backButton.isEqual(to: model.backButton) + && alwaysShowBackButton == model.alwaysShowBackButton + && hidesSystemBackButton == model.hidesSystemBackButton } } From 4313041ae0431d85cfa150999f6ad7662dcb7dde Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 29 May 2024 18:00:32 -0400 Subject: [PATCH 56/64] Digital PCT265 story PCT-135: Code reivew. Comments misspellings. --- .../Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift | 2 +- .../Protocols/ModelProtocols/MoleculeComparisonProtocol.swift | 2 +- MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift | 2 +- MVMCoreUI/BaseControllers/ViewController.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index ecbefc5a..a79c8730 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -132,7 +132,7 @@ open class TabsModel: MoleculeModelProtocol { && size == model.size && borderLine == model.borderLine && minWidth == model.minWidth - //&& selectedIndex == model.selectedIndex // Selected index could have been either reset locally or by server. For now ignore.c + //&& selectedIndex == model.selectedIndex // Selected index could have been either reset locally or by server. For now ignore. && tabs.isVisuallyEquivalent(to: model.tabs) } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 04a3a4bf..97014092 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -13,7 +13,7 @@ public protocol MoleculeModelComparisonProtocol: ModelComparisonProtocol { /** Shallow check if there are no visual differences between models. - By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. + By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be updated without a UI update, this could be subset of properties. **/ func isVisuallyEquivalent(to model: MoleculeModelComparisonProtocol) -> Bool } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 197701b0..5eb23107 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -273,7 +273,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol debugLog("Refreshing rows \(indexPaths.map { $0.row })") if #available(iOS 15.0, *) { - // All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to levearage this more efficient method. + // All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to leverage this more efficient method. tableView.reconfigureRows(at: indexPaths) } else { // A full reload can cause a flicker / animation. Better to avoid with above reconfigure method. diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 7977ddc1..d2638243 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -225,7 +225,7 @@ import MVMCore return navigationModel } - /// Processes any new data. Called after the page is loaded the first time and on response updates for this page, Triggers a render refresh. + /// 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(_ pageModel: PageModelProtocol? = nil) { From 012d677b0fc379c5fae332b0fb2e0e3ed73109c3 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Mon, 3 Jun 2024 13:19:01 -0400 Subject: [PATCH 57/64] Removing bars indicator animation Removing dispatch to main thread during initial template load --- .../CarouselIndicator/BarsIndicatorView.swift | 20 ++++++++----------- .../BaseControllers/ViewController.swift | 20 ++++++++----------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift index 2eefd716..3f3792d3 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift @@ -123,6 +123,7 @@ open class BarsIndicatorView: CarouselIndicator { bottomAnchor.constraint(equalTo: stackView.bottomAnchor), trailingAnchor.constraint(equalTo: stackView.trailingAnchor) ]) + bounds = CGRectMake(0, 0, 100, Padding.One) } //-------------------------------------------------- @@ -262,17 +263,12 @@ open class BarsIndicatorView: CarouselIndicator { refreshAccessibilityLabels() } - let expression = { [self] in - barReferences[previousIndex].backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor - barReferences[previousIndex].constraint?.constant = IndicatorBar.unselectedHeight - barReferences[previousIndex].layer.cornerRadius = IndicatorBar.unselectedCornerRadius - barReferences[newIndex].backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor - barReferences[newIndex].constraint?.constant = IndicatorBar.selectedHeight - barReferences[newIndex].layer.cornerRadius = IndicatorBar.selectedCornerRadius - layoutIfNeeded() - } - - // Perform the animation. - isAnimated ? UIView.animate(withDuration: 0.25) { expression() } : expression() + barReferences[previousIndex].backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor + barReferences[previousIndex].constraint?.constant = IndicatorBar.unselectedHeight + barReferences[previousIndex].layer.cornerRadius = IndicatorBar.unselectedCornerRadius + barReferences[newIndex].backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor + barReferences[newIndex].constraint?.constant = IndicatorBar.selectedHeight + barReferences[newIndex].layer.cornerRadius = IndicatorBar.selectedCornerRadius + layoutIfNeeded() } } diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index d2638243..d1df5e13 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -264,7 +264,7 @@ import MVMCore self.pageModel = newPageModel // Run through the differences between separate page model trees. - var pageUpdatedModels = [MoleculeModelProtocol]() + //var pageUpdatedModels = [MoleculeModelProtocol]() if let originalModel, // We had a prior. let newPageModel = newPageModel as? TemplateModelProtocol, originalModel.id != newPageModel.id { @@ -279,17 +279,13 @@ import MVMCore // 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) - - // Dispatch to decouple execution. First massage data through template classes, then render. - Task { @MainActor in - - if allUpdatedMolecules.isEmpty || isFirstRender { - debugLog("Performing full page render...") - updateUI() - } else { - debugLog("Performing partial render of \(allUpdatedMolecules) molecules...") - updateUI(for: allUpdatedMolecules) - } + + if allUpdatedMolecules.isEmpty || isFirstRender { + debugLog("Performing full page render...") + updateUI() + } else { + debugLog("Performing partial render of \(allUpdatedMolecules) molecules...") + updateUI(for: allUpdatedMolecules) } } From 32d3a20232da912de68772f966043cff28938052 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Mon, 3 Jun 2024 14:24:11 -0400 Subject: [PATCH 58/64] navigation line dark fix --- .../NavigationBar/NavigationItemModel.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 18aa904e..197c4f9f 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -23,10 +23,21 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc open var title: String? open var hidden: Bool? - open var line: LineModel? open var hidesSystemBackButton: Bool? open var style: NavigationItemStyle? + open var _line: LineModel? + open var line: LineModel? { + get { + let line = _line ?? LineModel(type: .secondary) + line.inverted = style == .dark + return line + } + set { + _line = newValue + } + } + open var _backgroundColor: Color? open var backgroundColor: Color? { get { @@ -100,7 +111,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc hidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidden) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) _tintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .tintColor) - line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) + _line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) hidesSystemBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesSystemBackButton) alwaysShowBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysShowBackButton) backButton = try typeContainer.decodeModelIfPresent(codingKey: .backButton) @@ -119,7 +130,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc try container.encodeIfPresent(hidden, forKey: .hidden) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(_tintColor, forKey: .tintColor) - try container.encodeIfPresent(line, forKey: .line) + try container.encodeIfPresent(_line, forKey: .line) try container.encodeIfPresent(hidesSystemBackButton, forKey: .hidesSystemBackButton) try container.encodeIfPresent(alwaysShowBackButton, forKey: .alwaysShowBackButton) try container.encodeModelIfPresent(backButton, forKey: .backButton) From 5e9f33efa82914d5d2a6ca8dac5471f5ef2f3e19 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Mon, 3 Jun 2024 14:25:06 -0400 Subject: [PATCH 59/64] warning fix --- .../Molecules/TopNotification/NotificationMoleculeView.swift | 2 +- MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift | 4 +++- MVMCoreUI/BaseControllers/ViewController.swift | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift index 414d3dfa..13cdc73d 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift @@ -46,7 +46,7 @@ import VDS self.accessibilityIdentifier = accessibilityIdentifier } - if var closeButton = viewModel.closeButton { + if let closeButton = viewModel.closeButton { onCloseClick = { [weak self] _ in guard let self else { return } if closeButton.action.actionType == ActionNoopModel.identifier { diff --git a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift index 7035ab75..4e1c9ba4 100644 --- a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift @@ -20,8 +20,10 @@ } public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { + if let replacedMolecule = try super.replaceChildMolecule(with: molecule) { + return replacedMolecule + } var replacedMolecule: MoleculeModelProtocol? - return try super.replaceChildMolecule(with: molecule) if try replaceChildMolecule(at: &navigationBar, with: molecule, replaced: &replacedMolecule) || replaceChildMolecule(at: &moleculeStack, with: molecule, replaced: &replacedMolecule) { return replacedMolecule diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index d1df5e13..dc2ed356 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -315,7 +315,7 @@ import MVMCore if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar, changes: &changes) { updatedMolecules.forEach { molecule in // Replace again in case there is a template level child. - if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { + if let _ = try? newTemplateModel.replaceChildMolecule(with: molecule) { // Only recognize the molecules that actually changed. if changes.count > 0 { debugLog("\(behavior) updated \(changes) in template model.") From 4a829eea7c7f72efdeabf49ae819c44fba9af3be Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Mon, 3 Jun 2024 14:31:35 -0400 Subject: [PATCH 60/64] remove test code --- .../Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift index 3f3792d3..191ee0f1 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift @@ -123,7 +123,6 @@ open class BarsIndicatorView: CarouselIndicator { bottomAnchor.constraint(equalTo: stackView.bottomAnchor), trailingAnchor.constraint(equalTo: stackView.trailingAnchor) ]) - bounds = CGRectMake(0, 0, 100, Padding.One) } //-------------------------------------------------- From 30b2eb150a7070cb0e82da231a11b6b50fcb6b6a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 4 Jun 2024 14:25:56 -0500 Subject: [PATCH 61/64] remove the duplication of the same fonts that are available within the VDS Framework Library. we are now calling the registration() funtion in VDS on the launch of MVMCoreUI Signed-off-by: Matt Bruce --- MVMCoreUI.xcodeproj/project.pbxproj | 16 ---------------- .../OtherHandlers/CoreUIModelMapping.swift | 2 ++ .../Fonts/VerizonNHGeDS-Bold.otf | Bin 54172 -> 0 bytes .../Fonts/VerizonNHGeDS-Regular.otf | Bin 50284 -> 0 bytes .../Fonts/VerizonNHGeTX-Bold.otf | Bin 55316 -> 0 bytes .../Fonts/VerizonNHGeTX-Regular.otf | Bin 53776 -> 0 bytes MVMCoreUI/Utility/MFFonts.m | 4 ---- 7 files changed, 2 insertions(+), 20 deletions(-) delete mode 100755 MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Bold.otf delete mode 100755 MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Regular.otf delete mode 100755 MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeTX-Bold.otf delete mode 100755 MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeTX-Regular.otf diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 9569da67..2cad7aa7 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -226,9 +226,6 @@ 94C2D9AB23872EB50006CF46 /* LabelAttributeActionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C2D9AA23872EB50006CF46 /* LabelAttributeActionModel.swift */; }; 94C661D923CCF4B400D9FE5B /* LeftRightLabelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9402C34F23A2CEA3004B974C /* LeftRightLabelModel.swift */; }; 94C661DA23CCF4FB00D9FE5B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA33B33239813C50067DD0F /* UIColor+Extension.swift */; }; - 94CA227C24058534002D6750 /* VerizonNHGeTX-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94CA227824058533002D6750 /* VerizonNHGeTX-Bold.otf */; }; - 94CA227D24058534002D6750 /* VerizonNHGeDS-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94CA227924058533002D6750 /* VerizonNHGeDS-Regular.otf */; }; - 94CA227E24058534002D6750 /* VerizonNHGeDS-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94CA227A24058533002D6750 /* VerizonNHGeDS-Bold.otf */; }; 94F6516D2437954100631BF9 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F6516C2437954100631BF9 /* Tabs.swift */; }; AA07EA912510A442009A2AE3 /* StarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA07EA902510A442009A2AE3 /* StarModel.swift */; }; AA07EA932510A451009A2AE3 /* Star.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA07EA922510A451009A2AE3 /* Star.swift */; }; @@ -441,7 +438,6 @@ D28764AC245898A400CB882D /* ThreeLayerFillMiddleTemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28764AB245898A400CB882D /* ThreeLayerFillMiddleTemplateModel.swift */; }; D28764F9245A327200CB882D /* TwoLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28764F8245A327200CB882D /* TwoLinkView.swift */; }; D28764FB245A33A500CB882D /* TwoLinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28764FA245A33A500CB882D /* TwoLinkViewModel.swift */; }; - D287651A245B338E00CB882D /* VerizonNHGeTX-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94CA227B24058533002D6750 /* VerizonNHGeTX-Regular.otf */; }; D28A837923C7D5BC00DFE4FC /* PageModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28A837823C7D5BC00DFE4FC /* PageModelProtocol.swift */; }; D28A837B23C928DA00DFE4FC /* MoleculeListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28A837A23C928DA00DFE4FC /* MoleculeListCellProtocol.swift */; }; D28A837D23CCA86A00DFE4FC /* TabsListItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28A837C23CCA86A00DFE4FC /* TabsListItemModel.swift */; }; @@ -849,10 +845,6 @@ 94C2D9A623872DA90006CF46 /* LabelAttributeColorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeColorModel.swift; sourceTree = ""; }; 94C2D9A823872E5E0006CF46 /* LabelAttributeImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeImageModel.swift; sourceTree = ""; }; 94C2D9AA23872EB50006CF46 /* LabelAttributeActionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeActionModel.swift; sourceTree = ""; }; - 94CA227824058533002D6750 /* VerizonNHGeTX-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGeTX-Bold.otf"; sourceTree = ""; }; - 94CA227924058533002D6750 /* VerizonNHGeDS-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGeDS-Regular.otf"; sourceTree = ""; }; - 94CA227A24058533002D6750 /* VerizonNHGeDS-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGeDS-Bold.otf"; sourceTree = ""; }; - 94CA227B24058533002D6750 /* VerizonNHGeTX-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGeTX-Regular.otf"; sourceTree = ""; }; 94F6516C2437954100631BF9 /* Tabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = ""; }; AA07EA902510A442009A2AE3 /* StarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarModel.swift; sourceTree = ""; }; AA07EA922510A451009A2AE3 /* Star.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Star.swift; sourceTree = ""; }; @@ -2440,10 +2432,6 @@ D29DF31521ECECC0003B2FB9 /* Fonts */ = { isa = PBXGroup; children = ( - 94CA227A24058533002D6750 /* VerizonNHGeDS-Bold.otf */, - 94CA227924058533002D6750 /* VerizonNHGeDS-Regular.otf */, - 94CA227824058533002D6750 /* VerizonNHGeTX-Bold.otf */, - 94CA227B24058533002D6750 /* VerizonNHGeTX-Regular.otf */, D29DF31721ECECC0003B2FB9 /* OCRAExtended.ttf */, ); path = Fonts; @@ -2749,13 +2737,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 94CA227C24058534002D6750 /* VerizonNHGeTX-Bold.otf in Resources */, D29DF32C21EE8736003B2FB9 /* Localizable.strings in Resources */, 0A6C1FC324927E2E00E64B52 /* colors.xcassets in Resources */, - 94CA227D24058534002D6750 /* VerizonNHGeDS-Regular.otf in Resources */, D29DF32E21EE8C3D003B2FB9 /* Media.xcassets in Resources */, - 94CA227E24058534002D6750 /* VerizonNHGeDS-Bold.otf in Resources */, - D287651A245B338E00CB882D /* VerizonNHGeTX-Regular.otf in Resources */, D29DF31B21ECECC0003B2FB9 /* OCRAExtended.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index bbde1bf9..be5c081c 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -7,10 +7,12 @@ // import MVMCore +import VDS open class CoreUIModelMapping: ModelMapping { open override class func registerObjects() { super.registerObjects() + Font.dsLight.register() registerMolecules() registerRules() registerActions() diff --git a/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Bold.otf b/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Bold.otf deleted file mode 100755 index dd3476af60d7448fc4a886dcc71329906d6d8466..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54172 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�p+^D*}?d&IQRY`36#If$`3Wb_t9AUr? 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 N4tK2zzhVXQ{{W43)8_yH diff --git a/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Regular.otf b/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeDS-Regular.otf deleted file mode 100755 index ce34c5964033809a6a7565f6baae1dc41a8c102f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50284 zcmeFYcU)7+_cwlTF1a_}XjEccO}t6K0)kTQVnq=|v0y2*JC92OgnStoHOUnyl2vH(4c-) zEh?R2sQ%tQKKkUd-ft+Xp-53fvU&vi^$PY|@iRs3ETpJL>wER+=|dlE=UD~bOEmqtk@5if??JhqqDfVX`dsRKFQm5DM~Y!y zLJoDWp~Do*v)PBhKLh^k#@aJmAk9z`k_-Q!4O9N7D@AcHp*(=Kw{WtSSu4a+RDX(w zZ^}lZqnA)pN+y{n34*l0^+}JSJZMjflE_&b3HfIOWt^pP^;h410sV+x{`gb!@=qUH z!IV?CplCTMrvH%S!b4BV;oF|h{Hy}py@Imw1q(>16jvKiGh&y0}mtfu9ZOHHPluK68Zi;b=6UY)P_ zF2Bxqa@HksddJ15B}7GzG3j^fJ>1;e>HEgT#+lOMje5TrLu6EJq~0$!%thZVI$Ce3 zmZ(oKCK?lxjo~gsj0sT_;$rmyzPFj0mdYw-q&DA)b~n=GZ_=d>7x?$2E8f45N?byB#hI?Mf?x48)C!t zF@`jKs1an3j7l^a6O7^ds91fNF~MYj=h&o#sKoH7FcS!n=<=@$@E!wIOi_kdePGy_ z6hne(LJwnNRAekL3Qg1hXDR5?tyN1dPT93@8vgAu7TY8D&fa^BJRB=!sgOPAn)TIm*HaG7`Sy>T8TnHkzWs z45a@)QPpnItrzJ#IV#x@t&jUQvaqHk4Ui@v@_3#&Cqn1Oi$mW&6L5v_k!;@>54 z(fdVw<7$Xa(~nOAKPK@9+Qvd)ime`C!q>1%2Aeo0E-9K!(&Q*(ip4?xT~>%2M)0|C zYb+r{`>Fz~m|+mdAbKRk>SN--OV`a+FZ_%lJRc*+PlepC!)fZ9=zA8AR)Qel(}P~E6# z_~^gmmS{;CsYGa*KqbR>IMf_sNkvf;EWGtp0Od>df|^FE2Ng&Sgl8m`1e^@e#vSUr zSw0;>mVQvTA1pTh&cWK=+PdXG+W)=gKl{@EYwbiB8yU6!uipR8<^R-Q07#z%e|i{` z0X{?nyI}2#}d* zgy_IJ`;3rkVHt(hTdaJDjmTJ%q4b+X)*2-L-(>Z*F{|q8oVs z*Kw0RlA$(<5pn-&voOeyfe(@8>n!}XA_pPbcF^fu|AXIO(6rMoO$@L|Rh%Eeijq_gbyr9cq!8WQ|WG#t^HK zK3#yL2|n>u2g(&bDVC25jPmah+y%x$#%`5>#3wS&SZJvSUc_Tf(30qtcoeY&iBnep ziiceC#<0pjM*rVNE8>B~_T-z4{VNAD$4E~G=syB_jfOv>S*vA?7WqgWt4Hf8N6RS4 z`{wIBB>or;79lUw zVfD2bi_}&vkaaJI%{YoDYat2F;7 z`w*Y8N43ZwNc#@Z>i_R;Lx4gHs>#5!9znD$PaEbbJk#OB!jl60cMA0By(TZ3CN{m{u=2e`{1e;!G4zi= z|NOIB0zI{fcBi|61Ow^OAi-pi;0j1^?>h;U`Ud(&dc8#g4}E)!1SWm@cM=5J4YylT zEkRw7py6KoI)!*|_Er-vC2(<$m<$A=ydo#0sq{(g8UulTtF(A)=k zsqQ$LADCv9q+vB?I*D#af1*DFu=|-d!}oW8)?Udsl>H(3ic){e z{u@PGFm9*|&#yF4fRg|7A6-5MIu$&32K74~NPVC^==#(%x*@Hl)wGTtME9Y?>Auu*x+b9S zf6@W;7`i|0M&(iYbO`N5`%^FJQ0gYNg04lCQg^5_>IEeO^q!@7z@}>e-mU;_+?G

y76(UbB4{Jj?hI)B(I4~C^{ zAT0DoQNyVb)JQleh_p=Nu`vC|(aj+MOoFgCftpB7gU~mf3Z~Rl2xSj}rX_>}PY4Lz zs0=y`=HY1CNROqX=y7x;&C_z4rDb#tnxhBPL#dzXo-kj%=^k_k+JSCDccfiu7itl; zlv+kDrk225xBUBaJ?Q;k{?Tx7LD4i_|6k!^QsOGsro`Q}xVMr>e%>(*qj z4-Ra^+J9~%ZKRB<-AHAsd$N&#zAd!`_<<&g7Hfmvv{ZAjdK(za02tj^u-;T^0Z6t2 zylE4)liEiepiWTdsq0iRoc=rmUqH|)PbIOqN9i2;DqTR|r619+=-+89kxFVy8c6IU%_Z)Vj*=ddzLLR`5J{9IL6Rm(m&}%| zlbr&oN2}QFvFN}%oJuOvxxbTS;K5&_A)1#3(R$WT?~KUp(PQPJVn%&_nKiwKE;{~=N2D>^AQ%FVNVE6bx>d&{G{cYM?YV?rEZ(hBfP z%hQx%O`FC5jB3e=0JzXf0Ayw%5~D1#wMsN58)Gdw7I@i00WN2)WaXR`nrPt+PYY-A zv~mW@%9#)r&Olf=lc$w4P}Q7)vT_E(!Z|!P4uEQ-3waXGmZya?p)8y&1mR2w!rAh) za3++MGodV;31Q(Jn-r5kIFl#gYBmS#4;uFD?9`3C@tq)>fpPsGT8dJlf4Kd+y zDX}hL0PueWPyfm=#70KL=@cAm#RH}fNOOeAQqhvK$hRH_CLS^x?NwS%;WwH&jEZHGhj_j(eSoTo%O!iv# zsRms`R-<-}dNs5)9BZ_w;aa0zjczr3YV@r!pvI6I!)t`p2t(UP2zcEL{snsv#Y)jZ z_AZW<{=(w*GX>nq?4_zi8JqJK9px9VZ0OA|OL>4Z8p!D%t7ME`kPjD7bPVssiy!J^ zFL!KuYj=^g^2FS%h>E4Sm7raIPfvUkRZ z3>}#mI7Ta1bzF9H_*2c3{L7D@^uEwFJZ5TY$~u1IZ&K9%Do$q&I8mC%X0F|_=78qp z{+K~QY@QhJGcxQ)L{;=KD}tiPiHkpb|;00$0i5s6Hq@@WPIZA;PG4c@87=V^r;=DNUfsZaOiu;pbdq0F06jKQhR*KsZ~4H%H*eT zE7kNZnRBx=xd*p=%+{gt;zem|7W4Ry$#?eD+39oA8_s2??lt9O1-c{kWYH+|yp$km zP)gF&F`D=Zi<>$-G1`qPrI*N-4ioHE05)ikTOJ2o)%@r$9Q{Q z9xHs!;EIm%O$2!v*HVzj)T!_T!-t1Q4A&xc z{kYVmxVY5KJ9lQ1zoa;=!gWFJpm&;zqN5cT+RrqMJ=n=rp2pp9ll;phi(xjhf*g z_^5F+9EF7wO`iv9=i(o_qWyO`tj+9?Y>}%Pc|AfBuu_XhSc1K<>p<{ZeX7<{Ns*JY zw{$_V%!9R`(c2YkG`P7Fs)2eSuO~=}+>x{>z+=7c35yz}HhYabYUJi&Gx%aw95z#s zZ$`6JxasmYm_Z{n$OX}NQA5-kD}TbeT{=u{_S#aezLR_S&4;{UO;&3w2@mh**jIy_ zHuuIgum;f)sBXLt)r)E3aIxC^;6< zON+&F^e2ZXzss2F5)>UAp_S8fdBz?l?o&JI3-;wlX&W}E&q6-H!6hASU_f;LmPh}ilUR6ihjKv+k)K?)tF?9LH z<=d8RY`AFE;-kFGn_r%Se9fT^6cO=zhG-1DoXPLS%Y%g1xHfWu5QS9bMX2sdZFfN@ z-@@C0YP>iktmd(>T6pOtoeTaKgBoC06!N@y+nF4#mmvR@*37owB(7x@uk3LwS zMWVb#=_^idyY&%TUn1X*R;qAw`MN~u;#EH`%G7LIJw4TwFnw&o_@yh7brWNyiU1z> zYZbsN4x`U1JQk_3r@SgeCBKNA&I-0WNAq$_<@fn?H+xUc0qsIoJ`~I@$I)25P>??h z098N0wX%3rOR7*|9|LaG73;Kl>;qIc7wOOdHS+F(gsz{oer!ILp22pwy;_0v?!3zC zD{^lEI~%`|myD2nM6f;O6SB=Psi1t=H zRK5%6ve$0hvO+5t>(vKm?SP9|xdu^pZX!BU=f(D(6c*Y`(`3Z`53_Wl4U4?G!t0{B z{3^P6ncj5=^}ECDl3zhxE*D>=(S{Ocvs~p@WFA^B4wajwX@Y!HvbZKijysh1L@J~$ zEJv(X%yKtxrd~@0WM}w`@1_@&)Nr7fdwrojJucL3vMZJC_G*SZW~kEqMWI zSvasnPVdcC`HOD1@wG)9IwLlco-)_#fwlt2st~EXl0mjrh6IROLpkJryA(CJFyTb> zKHag1f!n%haJXFMQ2vgSYYVvr>GIk9`^Cb+m4bZRB0*k`7Ow$!@Wf-~Vt}fxAis;c z-j=IkQ>Vqv(?)X_7VOQup}ykTgOi)(Xx)C{B>E$imx9mK4@^om&CrhGF0DLtq(uFz zlcRhFZkmjo3q z$nk+3=OgJ_0X1XJ$OE%arxa@6pQ8*cvE=!VhVd{aVt4~x{tqii@7If zx1)j5UhH^0P%1}V-QM9Jx&)0I5-xvBpM1l}Q6q7%N?==vSy zND(kqfJUi94CTPkb15pDp`0ZEkx?!*b&>+?j5@_o3n}U>1*jgtr+~gv9un#tMYX1> z^9C7|9C8yfIjY7<;|rzB8$ zJSE{+K%@c4g}c?%ObW1R0Hy)c1&EiLMUGgg*%XyQ!6rqbpy^n$Z*c%Hxh0slHH_C6 zb}++XWEs>AfOCWBIrLV#n7&6>0Id0eHcRSDCP*?RTO~In1(Gt!6Ul3aW?C|xnFs(W zpV>6Gaj|Iw`<4KkAvWi1ilrT;gJFkqfmO3D*j{WXI~H~yli4}!67~|;mK)Da;AU}4 zxwTvt_X&0q?y`2WzOvD>7}*TiJD?iAHTu<LB3!9K>n&`?V8~=C)V6o^Fht$H9yp%Ybk2k)tX;xzoNFHlOj}cMDe@g zkJ`0rC)eIv`*iJ#wF_#$t8K2+rp~T9kLvtVm#SO4ZiBjxb)D*VtlPcr(7I#mrqo?n zcW2$pbsyPMwt8E8+a|VIwy$j8*uJ%WuWY59sm!X!)-%=Ht726Ns%`Zh>SxyfK zuRf+ep+2QPqdu!XufC|htiGzguFh5GsSDJ%)Wz!C>bvSv^?mgN^+WX|4Xg3i%+egx zJZxm!s9&RbjfxwUHhR?Pmqt~>5AdQKB^(h>3pv6=ZB6Y+ZLD^RuAOd}&Y+9YP1I%R z7V1{%HtWvo3Um*3FLdv8ReDA*)pKx+)>z+0-&;Ra4;L);)Ac{#+x@lXE$Bi z^m5b3O+PksZ#KBu7%pC+0QxE`B01TEt!^WT0VDa=W@r@%{9t(g6mS(4X%e>&$_v~`MVXn zeQ=k!_jX_D(Z?geW4OmGk3}9kJob5<^(gRo-!uQ;36y`Q8B0daPRuI?8CaB_GO;9 zsD9n&a)(UqB2Fn5Z^=G+U!61IaBPUnX8Cg?F8(dl4Wk;ZYJc z?-9fc+XbYVUxsGgMH!`ZDJf-^h<2(jCHVFd9@E&N8QX;kd^=t- zM!+d!`3d~dkL`GT5IH&lef$;B!Qldw{faz8g$84aw+SDcVtLd-#PI_G}g}{_^uNetS#lWl@1IcS7SPu*%c8rjG-5z)hRNmdhJ? zmBPBNE0X31I&ajzWRWhh*>l_h)QIJ{9gf7&NP}I_$X9pwmz)I+HG=M+m!M1J1uxn@ z+bE#tqAh;@tSZDEUozXGOPpdQ9}SCN$H}+DldD2fYzv>=eu}1pEQM7e$M`QH&kpdn z&C^uM(-QRg+U36lS3; zRcEgFpcK!rz8d_mf3L8%Edn}UMX8>u6$G#f@`pNg8BJE9#0aVOBJXguz4tI z9tH>x#fJ;I4bdx8S0$~Q&=8dan-^6fZOxD7va?ps{Bh>W^vtQ7r)J_Zab^Q(*Uvip zERB`rFf0C>_NeK-QG6=zD=0egc#gPw7$mlTY$4za;zB2&cfIlz@Hla%JKu>nOT^Wo zLMk7Tk}e#~R2>|1zfwBnYH9RqO$@J79)BBG8FW)y{#dHKXWZx2R(heISC+G8wBWF_ z_1WIKYq_eiITLS`z>fbJ?B2cZUKu|+TYDcT!wS*@R&^P$HG8yH{6Tb+?g?7g*ITWC z-bq-2m43Osfb&J4QM|ZfQkj1RT1>)8UyJ*;eg?5%A7EdD?_b$|JX@Yp@GA(MXfSt7P#lJmZlnY$Q=qWhYyl}*Y_LIt*<%9k&ni&- zq_R=TRgFtwpK@GOPg3PyS+grgr%XkR=ZVWL?n;Xb4!uP+)Ly*uxOLZwqP3sis-}#K z%m~-C=AC2O2M*D>m5dx9Rs0N{T_{0oN$fy>yo70V0S)I~AC0hIrdy6{E{b0}MfPSf zJ7xLQtQm-%@hRz4_Z_lD+~b`Gu02ts8m-H4T`iu2U#ier6vKTuJleCvuwf25Mc(9M z@t0flxrZY4l<6#Zveh{|KiXRIK&AIO11NT>iuM!Ez$~g$Mx)g*vqlJBX!@5r&#LO6 z=@3hPKV##}Ewk1)#684aFfBd}<=1@XnKjU;?+pGK zi}d1xWt*06U$Uj)ibKnU72!)vOU5rtZis#`FOWWB(eRmq8;j*;V}@yVY(_%EX(7Nf zJ7e3dtccnD|IV>Y_W=wVQ^rN%3&a@OuMWa-45qcD1ueer}sPF=_MEWa> z2BGuI1k?wgmm-SAGesIrLBB~IS?q@o!3^#R!K5*(D6+&Ge_m14kDwF&@O~Q01AX%q zknh8V>+{RS3wI|83M+B+<0JteMk#*6i?8lR?EK{^gTA11D%1|2gT8SObY1#p+m$=- z)t2q5wtBmI2Vx{T$ClWDXU1@^PldGd7#8Aac{_E?I$X38-dbdWpaD<7nr4x4ULxSV zVlYJ1KtX9H@3B}OFva6r1!bxpe6hKp@DLouf&KYd{?5nm&_>)w?B)cSPhJ56GKuUW z?fI^vw>$44;AC;2>OmYIG#SeGz={BBOzF!jbD9g-AkuAwSpIe!Ht0 zLYF~fgCe!cV^`w)ZSJVSjeI&e&C)sZ=(qFG!d1ApTa_tH+sj`lN0%#R@km>s>Ysf& zk?dM4VAqPedRX?NV~2$6=AvyKSmmTIA?2L%bODDhqyESF5v?4tWbk7oLpEn$An7kB zJ+Vxy2tCC!{RKs+^{t;Tw#+9$d2e{{H;v+t9YD;={DPH;YuH`Dji2BeNS|(b?Pu)e zq1v_bS^ReNqY8I;Vu$QXPVLA!LksXP*3ms7q~eO|4@3D{;lsxxd`?x6s*|1cK6~WaGT~5w^a0!Lqtp`zl5)hq zg0^+4f>lEVJhuK2Asr7U`M@)pUs$dh3A`NHd%#x#cXJ%_-xFvI@$ZO4l5jqS-|^+` z7~U*}BJpQZaDS^1NkHb_mtFk>#`PPkoy%S| z4d3Ih={;yn(71JQWOSQTjI;7w1;ER_0}xF%!V?q?*+_fec4}oBSOLoJKv_1F-RUJL z%dpeW0DT|ac2#}9O~=FB*eO%uXMp8a3BPBOnY*eS!o&dqYF|w61qU5{RLbk=K7#UA z`b1uNIlZp{`Edf73@!7U3X17-c);453fNJM?9W50_#-4D#UV~4QTYlIb;KZdlCaj> zDMnU%>-1ARIq%xMMzuTc@(pQt_Jza(jrF*!Fy%(%DczOan4JUrN)L@5yrmZ)_C6c? z?jNp0X4Fe%JwXdHZpez$q7`VDbVJmNun}r}1=mug?VW!3q#CgV%0GQ5EqVt`-8penJadNzO z_ozx)ktwVYrlt`4U#);3TYh0H|D{VgN?FM57GMVM6TrO6@)9`1Kv(gasr(9|dJ!YV@)1wJw|C z2uwMtc-#InCFa4&Tt8NUm(IB**2|;cKos1{``y8xxb7$8^S5qZv^Z0HYYsPOdfM!Xnz5;?_Uq8o zcXO+^4OjQEV`VWzgf?S zl&43*OWCVR>CO-2@q{Y-YS(r=%r6v2G7Goz$j`h+3hSl-agDSsi)maw!!#oy!_;v0 zl-X0~Op(oH=d7B&YW8YVaRWUnhZR@%!~Ei^vCLv<+76mNWFMovWt)~*R$K>{3CoSu zE3QX4Pg=pEP8Pn-xH!W!Ydo->7QT?5acI_-jP0{FHEfG+!%0*td^;m~*7#YbhSQ;N z_UbwC$=EV`d&U+wRNO!@@G!qroXVh)XsZej#apG%S>%KgV8PWACy*dG3jKsSqiIqP z7T3f5VZBuw^_R9}6LPGUQ zh$~Vx5qgPz+<8v{$BDzgu7rfIE1>}btb|}&F7mJv624mrK>@6Uc-#^F8N`QT%X*E3 zGcxU&C$sqeutGx__|pnV^|UQYpY|ADy1 zD5UUT8qDLB6+XQ3Efja0F2F?E%pQ_Ypz^BzPfa!_Rx|eE4QqYVMjbqb?@lDgFAGvgh?H} zEOY!tP3G#wi&p3^&f(@|%$he#6F+lfmJUV1d!xsGCtNGQr_C(wZ8mqw{yim{+&u|F z!I9H?W#}-M-fFy^T6w&6amZt>A_yhkKp(Hu`|qRO_nG}-s0ufz(4$7E(SuK@0cz0x z6K;eXwbSDUT2xivVN@3vmyvhhy*qL5<;&xp9khx*LPx=mKRf5vNOELk`zs6uqQ`ME z|L$|jREyS7G6x&3Ph8hC2{$&uWL#&RjOj2L$E8>%N>UaCWW4%6_mG>5ckcMGWjD>yTrW z-dY6?g@CsH3ev-IPTyw?>MO2Snb$w73hT*DvF|d@bB?S(r&u-rp;%NxAA5y^7lWr)jtc@i~Jqk1i0;HKUEl17cthJ+*za8dEc?45KQ9WF%kZrNr(LPd{zzc|EaqI4B*y9m|w zMD7~Y1W8L#qn9B!n(fdL-ue{1e9mk|VJh5k36cQdqd|^{ zeuY{h4=j6*l|F-#!o$|_C($@5DlRkEWxdQQX(gMTbvEms=H>0adV7E0Cd~paJWbbm zu!`f8iqVS_IKZT{pP=`T7}VsHDlGf5>AnW(-h-)N=hzI4n09VRn7c;U7K|&`1RMK{9j+@_iUzB zBvfyeZC^8Dk19A9ZjFDyD!gnWUMPFbu0sdlWH=2uAeF4dJjY9HaY8r)*2%erCf>H) z`+U%2boz1N^E3QjWB~ghyMaVF)aVjw`=)np$F17M@LDa{arDd`HR2i<0qX`$dSI^W zv7rSC+S#1)w+`{XgBz>GA@yDR42F9yGT(DAU*%r{uaw;w*g~s(6OEp$x(AMSbq&le zDYe*Xert<#nu1Fk4rS|#Z|wdZHC3Zl zaqm0a(C!HxvZ;p#J2ZhQ&=CLd8>W$_^5))4Cm|@z)=%g(dW0-GE@D!k#(C(4vJ4$6 zM`u*cdiBJVOV3;H-{;fcm)SKsafvg=_T(`*cS=uOJ$BH1aFh2E!=uSeR$CFCsO}vpx?Q z)JL4HGS7Zk)w4G@rD>1YHgjZwoMQI;`{K!4^rRR?zL((c%{f88m~i%SFB zb{G`yK1`=rG3gfCeG^U3vpw?klxH&fsjH?w}u@8m>8Sv&^#m zEhmoMQ=^*A3Ltbixp?44+6V0G%QxOV$Pek#eaI+GX$`%z$t;uQj`DQ!9nk_ydq8NJ zql2~0QkZ34`Qk{}36`T-4;b_-@&X*~0ccMOw++ORejI*b9xUz3mFJULmVt_3mfdC* z8Q^XYZX?G#fW<#zwxD1YRxbI_8rf@*-G}GknJqB$7%O~(k|V;`@yAi96kWMvrr9n4 zoR+gkcAeRMNAs+xUlWJEevVH4&R3=rX*NwNK=%ugUlD!m0eW7_AiEQ)@FVAw?rMUZSgD-=_C^js*?w=8Em{7;LxIrNmc< z?xM~r=ia^^&fW9hyv@&j`?SE${9hg-n9u0u~uduI>JO@vD@%HKLC`Uf^wi$3)|{fB@nGQH0} zeSY%-jE2b_3sLksFDLVktlegSL9Z)&-_HE+fo&V-_e*UW$cYK{;HnhjBl7tH$JZwl* z;{~K!$^%I66wmL3TR?a)_6+8yHsf(Otcu_RTkvp8%QIPsb_M_ww;IRe8V)=T#j123 z1$QB}JVy#=_?_dL@HhZ_#t6fk^0@u9Bmo&n2b+NZQNn+IJdb=V{HF&4_Cok?ir{}H z?58ISNKV*K8^`}dm~R5!?+Nee7S@O-yr;(qzc%Fu@Z*HuLVIBd--GYSkKkMJZT~gT z#G^U$ub}f+Q1{a%DO>jQ=tu!_ysdl}$SaY!K2k_E?*(bluIZQJlhMEnsMdMZ?Q{v$ zyHs%I!(6y8qMW)ZkQaAE3RrKxtx}%d)K*ZQ?dk|$n-T_XXS?zWR94;i%9Tau(VQzY zQf&8=PEgVeL3ySSIgx8s%87;I6g0Ur4^SqsIa3I1&Qv<`KoJ&ko05ONfO;e&n-nyx zTGIH-sA-`Rp94|z&WXDsg=t^t7Cbs=soLyPZpts~d8D&c`Vv21P<}Z8>~MTnUOa@h zcjl8VwYRV0;d(>$n0Acjl~eUFoGmb%mgb*fIQkBN#GP~Dfof@?1oUtff9lej^S6O} zUuckX2zcid0q@C&fcI{~8}6Axwe-6Lnw}j^`dtD|ffCRPtEiK&+djXC4naGLQs#6;i>vhQc@7=B zu2N3v+DX8zEM0en_Gi+e{gkewCPYOAs-dbos5IS0I3*l=8_J&&o-G$naEhP!lVY<- z-U(j}<$vN|E*ChlnN1?ExL~;rg7`P^zh1l;`!^sQ`zQEP;L`bT;p&QQ;TxBy zu-+{!5htiN!(Qe%yBXz4QJM#*tekB&hw`{ooP2^;ZYuc|_O(w@8kYp+qgds+@u*uU z9|&B|7u&9X@a#?NyU6CzAs$^8o2ry=y7Z16jA`{<_SY@jDhjnH4;?@GK>eZfBUmqX z^wHx+b0E;aUv%o$Z<;432lmk^Klp{kjSkgf8NL8qv3?n!(}!Ow;N>U`O{4dsY0O?! zs`5WKI4}47`P`d>&-?og8BA7|gWzzBM(*dCGI5+LJLdX}>#o_m`#pa3Q`(6Z(NaW% z6kLm}w2`mIN)?&##t1G(F2!hI3Y{gcVQz~Vs&sF_my@syT-+n%9v(H?1GxOkVyOe@ z!bSRykj?fAxY1r=Kb+K%Jk-e2La_r5M&PdUff@Wm79E%=K*l=2=q+hX(v76YvHvsV z_ok}NEx4#tDSB6kUN4!?t+1midDFhhJ2iWDuG+FiC-xToq}#Tw+Vzuq@6=rhTeari zqPKKQ^!Qa{HF0AmCnxL7-OatFiHTF9qt#`G_ zlFnE@bIoe?>ZNOzFV~9RVsGiX^-C77)~sGLbH;L==x6ROT|Q&!Im}=9X%u97P!MTdamfk%(Yhq zh;G+l^g-xgC4CSbVGfF{str0)$tn7w6V)vKu&{uXmBk-t1hZ!TS)R$p5^uo>tur8% z2d5`Pup~mj8RFu1#Ruu5m3gxS)L`&H3pHpsze_-=8RUBqxg**P?ua&9E&T2!JpuRT#kpuKef0Lt+YIs>AsiNP zRQ&xDwTnb!0QV!$$f{mx)eX?uV1;Z{|r&SqG<_)SNuHdP|n>reF3E-$Ln@pH?3|ranIP zkZGSbuVC~Me?OQ@;cG`|Mubl?MeF?YMu+D0k|st?4I8BnSrxi63zd*dXQ&7HH zhbDURIg?7z(DQ}o(ePsWK5WHG0ek4K8hn|XH+6pcR5g4pNY@S+oXg_QI}pFdyRPrh{0Jv8cK@g)>?hkp1Rp@+;CxO_SA8aHR^f~iy0Q|6^F znyL*5%x7`WU5JGpmj*R{`w2B&q64UqnHucxtZCfq7HR<2G+u;Xg3JR$t_EwaBGw#t z2^(3Y%Dhqfo96ME;T~&Yw~Xpdbbp9{08CmBph#CR=Z{lLlWwBWe6+j7Ht%`z12p-O zGI_sPSLOSNTb{5qAzs}k*wAmZ_Kc5oW7h6%*`V$61A(tK`8hid9n%F}m&Qj&nMQ-Q z`x>&lYR06^m~PQ_&XGMQNRUqo`jr+qlc6$sz9O-`%_b(>j z-W(bW&gBXRvG@+CyBGX=!wJ-FmP-{&1rt9Xoy?`t{5<9yphf-=_eSKQ5oky*9)d>r z;SrnyD(vRb=M_9qHN-Y_vDk_!My*uGN29iIAlDYRJD!c(!RaLYjmm~CZ+qDLw;wec z&VM^d74tv5eU|&|E!+iuQ?C-bSi$3+*aa3n=yfRcJtP+~yLqwa3f>%yy)Auw%>rQ( zxS^Pl;?DlajtzoeWf!nHNFqgZb8$3#7BN!PE*ERqE~s{XE?QKreNkc2zHc7#wQAqj5BqX>2+p@?#V0Qz?=1Y;?!SqCz6ld#J!%R~ zE+RhkzDD`eVqUp^y`YT7o;J##teKD%y&hf{9nm%<>wbqc3?vN?pdfTymA+-`^sSm3 zTTkaN()DKJ=S`iQteKF$Y<0YDl__CmtY*--5N~o%TN%G&!H;V)HOd3)SI?NVPX|u$ zIF}Y(^Ozf=JxKf>56nZ$IE9`p=2nW{4C)2TDldE(5G_<6nE(Y>VG|^<7di~+yes;l z7=;wm`^v=+`;a%Bhee(~8?8OJXYc78&BZOF z0wEA5;vU6SYl>-9qntrYs?tUNavNU(E9DJ** zvgJ;0*>ZfxhFP;$Y8Mr=OH&dT#cC8Cg+2mm?3G+)Dx-^@iFKbbRW(Nl=F49$KC3c` zb-$RNv5Jch6;*a+^kH!ybJ#&8?yE5OS;zwJpD#Vk;qB;@bfMU>%)G5NhxXx{#Ci7N zdl2uulF^IXVzU%_jd+$>gYs0}ZgZ$1egQv%K`W{*k`s?U<_|LxX2fJ9Hk@i$$WJ~t zV_W*J8Cx290l1;?d32TSG5f5 zK!)8!hMlCR-+)32ahFOt^xSsRyIBRT_gIR)3Sn&yLhJz{cB_;cix7LNg|Kk=Dnu%9 z0yY}TEC)85h!DR;3Ao4S3i@+cA@ez`Dogc8aN*}*PK+_foIY(n2P=8_oDP;MviZ-x zSgkrWOgt~1H^blXVPdp7M*2r^ug}47s%BvlUI@4S#X2g|OoUJHX)~$wIoRiqV9p#P z#t=SGqlSv`ICaL7^?7)~AH!J@N{643Dkcx*w~NymaXqS1!NJmJDclwsiL1~k{29)j z@Ni^SA!oD;rdbX59v&c=7v+1MP z9*Di6L5i!$1~BsJco3=sfKC*AT<{J&6?KFkPR+huux7_~o#}$qG_c=P9}O(B z4V8X$TNj_$OU1t(tbB6kr(s>RiXD@SB9p6B`L>73=iNoFrTfTvQuZN$%5jbN-F|oRzw2_^tCerx4nx-% zjttRm94}R7y9N3Wu~Q>FTs7+d>+U!7%;#p zVn9Gt5CH)}%%ZZG6|-x=oO8wum=yyC444D!vSw}AL3zKbnGsO;oIUT}`|kI>uCJ=P za@9Xo=<2FLed4;=diOkf{oc{Nmky{bcFg6)^60(eH|@2_+Ojz-YjbpPK+MoC;YyQ< zSdcfngFlvTUcD*URI>e$^r|mkL%Ch^>1L_f)h}i@u@7^RsB_;jQNvWjqDF=EwlP_) z@y{d=j+XLm&fFHqeU-JjU`QcYV;_2y2R z>ME0AeEJT)>z@4mw|AJY-}mA8i?h9sz1T+x@ax3&SS-=le|E%o3G2`Auu!kEe~%9l z*7mjW_gjgtmBz@c5oD#f=@a~gDl2@7O}C;49vYcabAY#&YZe@TzhAudx6yGkRWsve z$7G~Sj^aE1mx-x!W^MR&XZn^2OXD*nGsWBHFI&6G=0N18e#(v-kvu;~ym9H+q@;0U zV^t=1OVwHUyDf)T9Ot{75b#&fUG6!~$IcRG#LgKrFKOzRr z)tJ3t7L;U`CiVosrWUw%HRV?Qo@*xO@ROfoBfKW9q;D2qKa1U-&D&?= z&#PN<-`v0}*Rr#(TnWr>@7pubSIPIb3W*ywaA4flg9kQjJ8)n_)IgC2Dc|l7aFv`)eikVv}(t z{gx`qatljx*GV-A=~qf#USO}>e|}ZK2bK8HfoBbn|S5i-_S&f zueerj#QmMy7c5(;%u5$fq`wpoNLrn%dXN8cul&ikviUeLtDQ1S<05Y~u~%F_<=FVq zBa>4k{04E>Z!0%$TAu8yG#OWTgFm%Yp*f`q#t&y2|3coM55A!Z=1;AY@~_g@@#j|Y z=hw2=umdxRw-;RFAIUXccqfeuKjHSdg_&8(tjq)3_hd;riaNAwGpLTOQ?nfxgH&mA zr!8D!!~2M5&zdz`S@K21dpq$RAX-OZ?V*t-K*>9U*p(~C9!ZWpT37VO=1qK2pGV3*k9@Cq z9+MpNbY16U>*N~W$5Z9m$Je)yr=*W(ysMMVUmL9EV$msClcHHZ59bDGQuvg4{PL;T zC7Qw~^UKrt<#YLDO>&xMx#UY9O>=op0N)&+)0%18<>X*%CV6ijkgaKkW&h@U+kim6 zou)aaA<}&celYJM;Fp$0c9(W5d3&D4{}~wq-n5^hl%6>%3=sMF^id!@tTd<|vedj{dHJ2k z_<(e{G}w)JykYqW3_T-CL(&x{sDO%8K!sg<+C8W6QSiSC_~Y=2#(ukVnABh&-%u-^ zW1Mt`w40{QX_mLVE#NI(<^8q~%UinwtAa}GS4f*UVcIy`xbchDY}~M6%Q{t8QPh}0 z!v@%x1eN4qD=W)4-}^iMnrU8I-BZ!6h;`?qFd|CSy7 ztJ$J{C|+afu8OhCN|r9+8~(QRH<6~zJh3v5m*Ww1q}tw1>OQ*K^H+^!7jle!D+!2!JjxGOK8vyCt+PScrSDldJ6r8fx^DR z2;p#Hyf9ffQ8-;VTew!ZQ+Ql>MtD_tOZZUuyYM~!hxjUT!f_d%q7I@?q8QN>{H(TE zv|O}av_rH{lr6d>x+c0MdLeoz`UDX#z|k4z;u>O=xR%%%2WNB^cNh1<@fq>>v2B9* zSMe5`d}C6cw0U6Lb`bCRo)o07+pSCY??GDFT#WGFMNhQl*z7%B{%3|(}`XZRZW8wMB# z8TK~}HH-+Fzj?%Q+swst<}6a3Nf!^8IBCrR8-BW&H*LBPJD(JOZJg3iO7G6zzvH3p zt$o9~2L?@UH&K)ml~7&J~xQ*t|$MAG}q$ zJbu`M!M2S$`8REHq5J);Evq)JQOy)h51KS2B}tMpE-5w6c3{$4a2oHy`^bX^M+Wtd z+;RNKj-AJk?uiUin&4+;ixPa_`PfG4D>9MCZP+#Di0y;R=L#QoI@`MM@RZoZ-=z08 z9>20DGbwtm@|x(}HO3_-rm z3i5?aMmA|b%i4!yIp*+2@`m^+-nAy?s>d&6D2{d!J`uTp5}Mo*-JW-F_X8}{4eK5d zI==a2mBxC!OPt2oMka4?_4iBJo%mJ0V$DF!9HEc+bT-bA;A>&UVU`BFy0qVa+!V6i z%U5pPYm?`{*UPJGm~VJy+^*3|91GAqrbo|!;b-sM+k84FXEWADWsMc%6gY|mTXWN} zH5Xejs$cZ_@EJ3+>U@pPg|%w?HPzHm^2@ATW4m_h6m#L_t4-%GUEI{EAwV@(Bfjdu zx1A{Q%y}s0t>?eK&KukCcFtQh)*F=kRPo)^LxUFhO74rs@=RweoGg-L2Lv8hXe`qj z`Dg?-8jF{aylI?LvlyR|HO!*^IjMh0_}B;XKax%)wzX;H+pl?=G64OS#eLVzftxPc zT+iNfKLg)!+VV&GD=gk`ENO<1pe@|UzbN=c1_pKpw2;Xg;?Hw`{>X4eNj*m?eo$Ka zpsY1Nr^NVnnKAxu@wkiQI@+{vrIAn8)Rh#7=JV*(jPKl%oY5Tb$|uMgJqUmJ_|E>P zC!%-vTc*U>9-8ia)-Wmm+)2tu;fxLsP4qwjY?O30Z(JYvYpIYml};C%n z>Jqy>CtW3D58dNu7UI_`>@zT&f5(rnWy>2qJo$k)YP7mmrpkR~p9Q%#`!;R6Rj_Ng z?|ck>-%jXyP9wL~73w}`+j{iNzI18NmVmj>nW+ zY$n^bcav?~>&dq5wb-^T57HRpr?^6LY(_5cKt9Tw@4&b8kE>-p6`E}=dt$*YjK4Ec zT56Bo0j@47sGmhK{w2L1rFe=}+n5_AEs|w7dtKw}UG05C)-Qf&&otFpk!*+*fbVfp zqbkHNPnSh)cvUEtYC72PO^@+4afSho;b>K}g9f0sF}W|uCYvQ?wvGA>b63g8*ZEb+ z>_+mnUBHi#$rDg!`jREnSK3}ax%>}R$&qGd2gOk-qlc!Z&YU#~`=lR~Zo_y)<0&7r zcKi6Fw&xCPJaQ^<+2AyCbPn&)N1DsGIsGV?-JH*RUllyUu|$KUGCmpuMh`wp)@RcB zo)=48&dD%fxhle$8mG>%+sK{*0q-Oue;2%o#y6H_>K&_Vd~1xAwz6ae z_KeCfHZsAwLkrmb7DcM0>_j*>L!(?=8Q<<`y3&M$via`G5R6~S_#RgsN=JUgF46N{U%I{Qdfr@ig>Fp0F2*4n_hm)<6|xd+ z3YL{DmCLkKC;z4ze|*+#KUsQ3RMH;j5|t^ww8b)fiK4WvkQ}bzt~*@gD*rr>e|(uO zdMGH8U1U#|1Pe~eKp#OeQJf|iDK_xILL9X+<265Ur$RRN%CMx8fmeC|WOJEDiqFEb zWNc;1$1x`m@WIIxAs=Na3LH|KeP1A3JZG_te|d$Rm+?>lh8m0GFeI|qx-AemV#7mw z!bZ9j-#29j9ZJ%1eg<&*UUrAw`%?A>A8=*LJ0clx%;K;Ja(o6k$bmi_&W`8>Wq>n+ z`@jr?GQbHLb8*lFi6_Nj5@#3#pAKCD8JqBw_-Mu%$1@INJflMsV{pzePM*No&gJA- z3cOdi$l0|OE)HiwRJa5<4MGwgmk`IeQ`3u@t*GfU4A%`Yov7K9n!(h>2^6$GKQl>9 zN%=pw1vudXH_g%1ERG1_;5NYN!&`8OT`qMA4W5XKs#)UbgI5e|my4PU(Qel60hs)nFSkIfSr&U308VfL>N|%EAQOx{fcycz| z2zKr2*fjzbf-h+vBtN36;O?7`9)6t7c$&7hT~C~@jpJcR4AKhJy7))>_}8I&5F^%4 zuv0aZl%)5mY$N3pqJJK)4MiB}v2rTyJIZ~h_eV?${fbyW4@JEF6wftqvN+KMU%60) zv<_86aaMoj(2Be(-YA4BoZr3uEL30P@1p9QKM$oa(zj3feab4KO+ zW}u=L&=XMFpO6Oc%Rl2>dMVotZYdL2UdSXNbR0q^l;<-=z}hSL7nf(@ta~ZzSN@Xi zR9*l{+X6AhfkIyZOS%{cdBXaWypV#FREStlkk32j74jkU`)7FpN_v7*kD4KE9P-eX zR*YCrP+9?E?ZO%7Qe9~Uq%_tarFKJn31WXn-3pl(uu1G!&SXe$X^|12+rZ!vPv*0~l^e@4AcQQi}TJ^>s^E>94n2<9uACQl0;Z3yyzNAuvA z-UuB?d!B))DT|gCqvU3g{Nu{=fRkpZNek$4h)3Ebpb#e|6Q~Q(b^_^qv<5Il_ynXP z>330=fQFAlo+LKO?TI!wq$03HIMUK|@ZqfH5-O;^sfIo76le@uA(D9m_#&(PJa~z` ze}+6PMqE=+BIyaa6%u{4-fIJ z^l0TZF`=|!s86Mb)~NuwP)S=UpwcVaOxpIOJQp@n1HO0wxt~1X79uZc7>Nf6>K^2f z4{4%>1Cg!YJ)|uaUix=hTdgc2(5q5+8k>Y69cjG|vIO3`2XMus&+D7-PvkjbH7#iS8&1(0(tqk{f>3fH+yoYoTU1dh zMGhnkm?3EqM#el<;Go0)H_VYTDGrt+_EzMCBhbn-5DRYL*Al)2KidA0^z2W{^T2bv z%Joe|C@GKNZns&UY2UAqCBN)n7hjJB7ypNs;3Ds_-6Bn_>pR?2`@fh{R$ zeHZ$V0_Z2u8Ne{j=r@n*Ef{V;6$V1{tN$tN$Nh&AcMq6|x z72ut=zjAE}wrOfQ|2$7A@&B-bt@RB+e7W)RsiT@)3iyKsTcUKjBbI>Mlfz z^CVxCr0X@||5F~*N_&zwF#H6mNb9t{A820J{}Arc>+gGqpYRi@ueP3~zHog;&7|eC zP~YV!fz0d(MG)6=+6F1gk?O*hQ>x8H>xkM3i0^3uoErr?ZclChH6N%LNeHEvrX{pBvS z&Xh<7q$jvBg`6YU0z|%*q$}`0vyh*bs*nuzf1;ycf*%c0|5p73lRFy;DviRE2P0oDqT!2&KO(-%2RupOm4^ z@gK(ghtYH}A?+sAKt_s$uPSh4x)-w!Vo(C{sCMF zZISUP8I$NnDwSjD;a%AeHP`mMRXzWS3uDorJ*WZ3*Ca$s*Oe*oUr@RppFdz0Eur*H z>45U6Ht+uq-uYSm|4V*VU_#{tX;-DT0X8~#{|i_UY9e^lkMtm8sa&EwM`c(6?I(x? zgT#VdBWr3T6f&MD$UI_$3jRy2`R_?wqf$0C2zOIDq$54Zz})Z0XMYD5+NTiw6Q#9@ z)FtC9I>rLDwvv))%PHh^u7b*PkV1z9RDFchNqLMw?;FOx#iCLPt7G3nfKy84KZ*M- z(YF_E4Pa^|DXDiQy7+JA%WtJ>U(jNVu<))OTWiNuB#5kA>Up$k|3wpm0?AmG(i30{ zs8jC>gUoTXdDqhCP5V-9&NOd|v%krkrY5=5ynl{$;;XFzDfWjNRMe%?8{r(pCH*=@dh{wHyj>GGF_iTB=rz)Iy6Fx+<>4F`%dIOE(}_GN-1lstI*9B9NgF z)}};mT3FF>vCbuB8dRYwJw`Cf{>H_~MjMC9QvxeOaRrRBN=gE0GLFQ2oTLOiC^rD6 z7`Xxp`n9Zg7=K~}`_mfhU|Bhg(&PQJEe)S~x=fYvYjG>E1H* zZzMk*pW(E5)LY99G#B8U<^X@Pf=M1Sy4K}JoTRl#uUiqKK4dPAb+7+=N|GD$BK0AV z)k+&;exp{sKcZE+ITP*CLx9d~XhtM2Z63gaK3pI2%_b$$`Eq#%SxSGw6!JDg{-(dq4KXnrV~md8Y6t9(XPso*~?34+Jj&wn2U0@ zmY*jvQ7TQLmACYrv_g-K|AhLKm8lQ4-Gv&W#wg=6T`wZ5RQo9^+x9u`g)PE5Iw=z?@oXnrvH|((g*AR_tft#QS>Q5(n!p9Q;no^Q z`g`NcD~DS7;3ivgnL-%yCpXwCaW}yfgr(u`1>A7J7?2!k%~*szq$NHzTI(nmjdS1 z00SA|Y7E)T0uqL3w<(Fu)IwxQmOmO<{OCgqsH*A%%5 zkvHlFUopyJP&TOnhn#T#19IemIRZHuZMiXRxs0}4KwFNR63}u3+Hxb>auIF0n6_L* zTP~(8=V;5(ZnT`EEjOVprxXOJ)n)6#Y{j+)yxOpB0k5vOVVK3uw0&s{Ww;MmfI5-Z z5=s_)QwC@Sc|_FIzF{Z9Hy7|wNeL*FWN4=elqp)RS7y2!iOGmc4rNSB>H>I3L3=VN zC-~vk3{aspOchjpgfzgr8j6J^^FYlSK?(3Jr72Q2gA(HUkmjge3n*vY#?czEZi6c} z>VlHJk;(`688^Vq%w3SWD{eM60$q1Qt$N^&VM9i$`t=P-@%@G7`Ly1m%i*KFEdbqwwv| z0US9Fw@8nNstw+pfXg!`;(iTBT(dC+H!-K-!juNMC1n=!oDJ?WqcmYsi6%_&CGQ1p zq%MN821maF#PQ7z5c(5JhVOCjakq+kR7EV1*IV+r5hEc7zau*wJ60n zP>K;!im6TMqb{We2TBLECU1D0B@ebj>Mrr4+g{3Rfa$9)ddv zMG&c`rA)FHi}Zu!sfHNZhe&u%r*&RBi^x0lJ217xWQE6sIDbb3;#GJyU28D@;!bD7^nSfHIgu=&=!beKs zV@RpefJ+NR6sk59 zswNbwrj%wS6t;$xW-TcEq|9prNb^d2*4oiMYf*I1S_XwX*|SF09Si_TBf#Anx6`X2 z8J!>n8v-6pD7PX4jYoE~83Mv)kbHKKrM2np^=iP(3-BR$)W##@DJk$^j@=FljAa~w zhX%kn=_6W#1_f<{+uJkEyn6=PGwlLX&o9Goyd-*<`inqq~-#0$uhf~n(L^!jhg$&#oO#rYGzaO95wR> zRdl+hEa1Uxp1o7)O4Vxa{?|q7r0Wh5jC4pvrWRtxCB85YIdP! zPih7M<%B;8zy7^VMj9mi=Qh1>3pogAJ{2YznQ40}38;?rPDZ-^hscWm{xCA$BrW;T zw4?r5D?ljOkEGpa`7@J@DOvE*_a<3UrIh=fX+!;gG(p>l`J<`rTYogIY5X6}>eT;7 zldNX4;Lh(&Tk2EQbijpg{*Y$_aBo=>WKbGp%p&Zc+Q95!4q(^j8Qcta7xRQy==uI) zIgDb=ac!~#TaR^To3ZWCQ}twnQ5wn;EYo6Ha98Wb41xNy_K+ADy<<0)8Yz0RVbsmC zgLQVC&K|3?r|axxI(wVWKB=>B>Fn29o1?d|F)UY`x&@po*M$CPqe^%%W5|vAP60j@E#Rr)dT;&Vcs$&OeymhqhZRJ zFBtuiIk!3fjkILttUX&3d{T?f|5%Pdo6KX~p;*qIy0p8ttK(us0XVP^_M;O!3>?V8 zW+8GhPiFtpne8ys{E857j0IS>jEsEPcIYozwgojkNzUv?%;H$wSxC$xm>kTSNYf6t z3Z~<7#Ob(+aVBzDfYlLF|C|4`1?&q@u^oEtdKgc6F>NsW=#DwuK#X%Z+y>KpkAb=999h00FPu$ zj~#=Er*)J0OffV^SB;1ZrNBIe+K}FjLrEv`b`y%EBW+#)H%U$6kuq?tB-BG{`cRYj z$mtr*JIK!um^XaFtUByG^H zOs%1@SkMj&ESn&Hd&qNN;KmPW{86KuwDe-;pOsJY_JPv2ieU!KVs5*Gs)eVTsrviZ?s?!V*;7%&7EePsosY7gODmQu)o*s;P(RY4uOuJ zG41h3LwjN~&&q#?F2%bD*K%6X{OTcYy>gA973}?5Eihu7f1>LUKLa)L?TdIpj5l#} zv+*8}yoe5i&S0ASpocM1K?dqS6gnAg)aw{ViMlJfnaBg}0=HKGJ1y{Is&jRax0tb^ zx-M)dX!(ztt%G`~;9m_R_7jzL`E&eJ@e#Td{D83XXYB8=Ph&j&lo1MSkU!FJ)yh9o zo8 zg|=dyfiq{oz?tbvY~e%B=lOG2X31&wp?fQ ziD)l@ABT%`b-H7v_r`2F;|cAqw{>B)*=e9VHPf0s&B(ZmOf9xY`DaRlgceUQzTkUj zlx0F`(vhhzXuwoAn8Z}q+u~>JnaY}TU~GsRW5c$Lagepv?)4 zJ=+TWvJ!Bwiy8PMtU5p=tpnQwWx*DtF&*^zl5`knrx7>coC19vdJOakotxN`pszvC zfSv|T;!Ol!ZG;`8=QRM9^fhFZ1Z_7JHW?$~u^XpOwM!8h)=TS{hX3lsTxP0W6ubs= zEbAsy8;XQ3CIZez$f!d_QWp`1KdDVP)*v%L-9uf!l8@aQXlYUAhs zp&5I|zt5}A%aU)1%9^W*&32^^^7=iw=&-NxbB6dmn~XeAhou>^taFd)VcW z_RX*PKIJu@_=q!Pw}(!d*gtC2p!Sysb{HRc%3|#4ly~P(jo`>fptY0! zQcvP3DitOMRtA~fZByZG0Pg%QP);G=-p~I z&1z_3VqCL^4P(O-;#?Cc>9~f+4yQDxuwdD81BqG$3%!uKHBhKf8F;BZ)r~UUGu3IX zy2Rn}(UsCR)KXMsg7R+bia3;>6^;f*YN@^k0*Ts$8bDS_c|- ztD&~mQcg9JA*GiF?&?Nf4csb|2lz)YX%Zta{B2Ikn+bi?Tiy)a|JPGP#gT5QqW(=n zHuPL&HS3jKhM`Z)r{RADja3(wM$W30ATy8k-MpizSxLR!zwNTgnf*tfMa=d*qpo|d zx^0m3t2esr$!JCIE}J5%|DMt&%yCi7*|SZeCfAhAZu_e6PVd%=WWQhQS&0U0@QQLU zU)!U`Hb}f|HpYq%fo6nuPQw3-c--S zVgC^iHv%D1k>T?1csL+(NK8aL;p0aCmXFtj%bNNeI#O$+kKYlK^3`ucr2+GIExEV7 zzGZzt!@}3uF5ahB6%5@syG^&( zQ@swGov!tMP|#bsd#r!&)jQJHakuL!FL!=i7=F`wntR_}1C|DFTH)N?#QOE3d!fzS z_fT9h?OSgHy{C+OJ;3_e61p{}QQgTa+L9!*SWKzTel!8JiAp(&#``4*Q; zbE$|M8X4;PUDSq&2?-6tLmLcncMXq-SGORQQyDZ?d#K$q8)i1BoSF!Pe~}uZ;Gc52 zdnMu4;Y{ROlTMb(n_ceq47YAQ!1Ji_3$=<=qn1H+wbjI*OTVhUEIJXE8P$B>yn0NR zw{ybOO-8Mq;v|l(zUP7C{mdH+1Y6Sl9ak-DBd{yXdzx~w=1|ey1Md5VpZHk6PPoT2 zzd^%|U*rri;f3oOM~|Imb>{ay5BL0DXn$se>E(rq_Yd@YaIA^Sd(?|j`!72i*stC4 zu5)gB-FYSxubGt<_DhQCHo&BPhqf`syIy%M+uiK0;fQKwb}9QNj@cIZ@yqf9MpiEG zCiFFI`CH#7Ru?QL4QE`vj5pPtQ~&XvkHMSSo$}@N8~WMab2yUob<~0$Icd97)BC?| zxv$6S6b~2IF^BKl8(NI`BdSKtCN%7>Ubq7zfq?_OJ3XI#;dPBnurUmDiD)AwR((uJyW!dfi{&@t99=@k(l zNtnI9yaNW=oGj1YHyNOV9`RK#NInPG$Fy>!@m2-u;|Ee z`|k0P!$ad!s*ZH*+iLSlOd;~9F&WpWJ>A^Y6*AGoQ{BYPQ!f+$ziRBi8$|7_Es@k6 z-FW3dqb)BxH}}6k_GpXtc1K(ueXYxt_Li(2G*#~O>D~5oZqxcZ6xG|Zq21m&C5L}G zxcgw|y zgEw;zKVzO1HrrpLoXh&y;`O5$e>9m<{PFg-_n+Uk+*vqso<|LZU7L%EwQJOzf5-Cc zwt79!&beve_iR?UZ+eT!$rr!+9&;Jr?exMOCM#}S^FBEw;i;u${PWhU5)OGtuk>%- z=+{NlUk4O9%5{UNJ?cq2)uu#{P|+(?3m|iU7?=##OG?(rAXFW)x_@S0bsu$a^)FV2 zpt~Uv4TnSy#>_ya7V9P&29|T4=i8)hn>Bk_&XtK#H#%0YKc_(PR>Th4vBS5 z0uLpChln(8n3O>MNKRyYt{v1($dA~VU08hNt=jN|chkSPogQu;-|;9FYx8S&Qip_H zDX>}6u;r`!OB){N+J%uSw?@yk0Y=e{(;Dzz2Q8I3eqjl5$_a?lY5>apA^kZoc228S=+@RW} z2VXX&)*J4uGM42&e`!;v#^aA>vDNl3EPL5b=zcG6MUMf_2Ubqa@B3!`xb|)0XCE{f zW+KvfH10U!&S0nlS8f*)qlUu z?AE3~!!gw}cmT0>Mkm%9dU+q&TXt(h^d8%};Z6?sD}zji`PPK0QcX8u`nws7Zo&lF zRx$diBHMm4#nH>BfR1h58+&3xGzc>upt{l&3xjF*3Q>gFR#Wx2;aO7E3Rx4iutdBW|EQ>N4`P|dh!-kLiQ)URQ2-zF+)Boj^Qe-aLW@`2TK_mB_if^& zA$Lk=T z?b2*?jGvH124Uf0l;`0flcP`)+1?;n9i;9h5)GMTg!Kt^7j-SY)ZthwYmz(xv$a8$ zlH-gXwd1!)oVte5Pou@hh5l^*B~bU)r^Vp0v)T^>D&1@mgGY<1^&LbpOO( z63+f}{MoZ3<{n)B{PxcG>B*yBt=%p+GmrT7TwaieUF4Ui=~mLWlnSzeZZcQsdfXC4j-7;r+%+7WA+Oc?EMsQ zWnkj65AKpz+YcSO;5DsW<=*!C>PK!K4M$I{_qzVN-7PIH-|uH@(RSSINcWrq!Iwtv zO~)oRT4vqI!msn_ywKk5kA5zfHZ`0#;h|>6X`kI!Lk5&L{@iiYW%Kj}FPnwkujYEg zzw^u#lkvI7hxX096R^B=v1e>XbCK5(c4ju)!>s4qQ?u8k=5`#jC*3Jv`RG@#7iU~4 z*fxE{s7ctNJLZeo!6bF6un(kMTb+b!IqG(4lHb5Rq2oTjRbEJ|_Z`U8|6h$|$Yj<7 zOH$gotePxH!C4_K|JTL&zZ+~Vo~&f7cGlm0U6Oly==>f7l_Nes@!fYXsB5jTsC_4b zZF}%*jjJaQJyPJgwPDTfcUOLR8RFzG%U*oWFmC?lyZwy)*BQ^t+*fRwH!nWWHE-=b zpWl1hwr<e1E1(Y*c)|fm4>hH(D^@ zWUrn~$4mJo>&HE4c-!Kc*QO~qI~=k|%eGzdD%h*=w|jcAIT>Peg1Vtzia_ktBEu%V zyRz7EuaI*;U6G2`%FGCLIK-!HU|L5ut)x9Gwic*+s=tXT2u{7+O-@dxvtLXJsOTlK z(^+3I$EodX{w^jFpK7f*ud8-aJ7(6(teK|Li$}MLdNzy;5A|p;q%oE0bPVTcKzeSS zP!(|B-ULv@%(0g&QcWVl>T@$kW-d&sjwg36c0|bwzt!$mByv=*f*nI2Ck;V-@f;MIJx^}K> z^VAn9Q>@CIckTB5pjgxD^{;m}Ojy@zee3O0T2@)+iD0MG6%|A-#xGC+GUO=en|2crv+!T8k?m1eVz5i zd1oi{W5#+C&!#R2Of8sGQf00PNGoyW^v9mhux~= z563Qh-+0$$bL;&xrtBQ_{;Fk7w^V}`5UySv%c>{M|6d;-a@Bqqz*c<0$c$50tMtXf z$c+p?YJB^iU?^}isuXO10b#`l51E?@)|o2Ya*Q$b-%Jb*+^~w=Zq$jDrZe+HTKPq^ z*>mM?+=#z&Mpy1z$ZrR!EVFXJ;_-94rj^{kmT{=N<)Emh^9Q{ZOA20hpW7=v@)zrn zU6#E}=4bY3UVeO9dHV-l=T2KQrEyoQ+P4?}QtY)U`a(UAHoq4Ir};O@lWsb?KGu~T zHE-6WL;K>H)*Et7XRKaba_@M)@b)dQnhmyBe|n+d`k3e8}uh2U2y&o@)>g}2v%d9QWu5048z3|@7 zT8}#fZt`|sTGuE-kTP<^c4x;>hsFKP*R;(x?B4p>>QqjqPUXZE&=a|(vLz629|;Y` z|4*;)a@BrVV6H?V{UJTRp;h1}`{od{Act=uLN`-%tX^)IQo4CH_ELKV|3D53*R%T# zhAKPCY!ZwYb(F4>6igla6LRobHfg)G)xOvf?b?QaDzG1%8$Ld2gT)5<`$ct3=gZv; z-mAX63R|*dan5t+KQlC|2hCePtJjB4cl$W^UGpq`}cNuyRXS|x3am% zj~lue<&}O~>J@Ak5OVlo`+5?qUQ@VvyV6uM{anLa7^ZSZmpyW>7j2mNTd#T|k5wwm zYRt)1(|3L-x)3(}%uR=P9oufGwcW9nW!bH$cQt1|c3N3D>({}pWDn)5>}s>EQ=G16 zepxlkAT+KyTiD=WRPPyW`j0m~7ML_+{H#IEo0Z)j+0rYr)wcOJPj_z-V&7|;!~2r~ m6P2I(HA;`!zJ6KvK1R~6opYo9nwjTnlh$Xu=+QuYf&4$9EVQ@) diff --git a/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeTX-Bold.otf b/MVMCoreUI/SupportingFiles/Fonts/VerizonNHGeTX-Bold.otf deleted file mode 100755 index 59b1f438744b24d3f81258a69e52fac419e39a86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55316 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 MLDurIPMCxIKZ(ilegFUf diff --git a/MVMCoreUI/Utility/MFFonts.m b/MVMCoreUI/Utility/MFFonts.m index 30ee68f0..10b0e8bc 100644 --- a/MVMCoreUI/Utility/MFFonts.m +++ b/MVMCoreUI/Utility/MFFonts.m @@ -25,10 +25,6 @@ NSString * const TXRegular = @"VerizonNHGeTX-Regular"; + (void)loadMVMFonts { static dispatch_once_t once; dispatch_once(&once, ^{ - [MFFonts loadFont:DSBold type:@"otf"]; - [MFFonts loadFont:DSRegular type:@"otf"]; - [MFFonts loadFont:TXBold type:@"otf"]; - [MFFonts loadFont:TXRegular type:@"otf"]; [MFFonts loadFont:@"OCRAExtended" type:@"ttf"]; }); } From f6a46eed00ed2887a4bcbe35c0647c40d18b34a5 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 08:28:09 -0500 Subject: [PATCH 62/64] updated to static method Signed-off-by: Matt Bruce --- MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index be5c081c..22f65ade 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -12,7 +12,7 @@ import VDS open class CoreUIModelMapping: ModelMapping { open override class func registerObjects() { super.registerObjects() - Font.dsLight.register() + Font.register() registerMolecules() registerRules() registerActions() From 7acd9e1b778aec98ce75a64f8e81b5f261421cd5 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 5 Jun 2024 12:30:08 -0400 Subject: [PATCH 63/64] removing dead references --- MVMCoreUI.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 2cad7aa7..97be87e4 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -743,7 +743,6 @@ 0AE98BB423FF18D2004C5109 /* Arrow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arrow.swift; sourceTree = ""; }; 0AE98BB623FF18E9004C5109 /* ArrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowModel.swift; sourceTree = ""; }; 0AF60F0826B3316E00AC3DB4 /* MVMCoreUIUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUIUtility+Extension.swift"; sourceTree = ""; }; - 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = ""; }; 1D6D258626899B0B00DEBB08 /* ImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButtonModel.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift; sourceTree = SOURCE_ROOT; }; 1D6D258726899B0B00DEBB08 /* ImageButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButton.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift; sourceTree = SOURCE_ROOT; }; 22B678F829E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNotificationAuthStatusBehavior.swift; sourceTree = ""; }; @@ -922,7 +921,6 @@ AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingDelegateProtocol.swift; sourceTree = ""; }; AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = ""; }; - AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = ""; }; AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = ""; }; @@ -1215,7 +1213,6 @@ EA985C3D2970938F00F2FF2E /* Tilelet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilelet.swift; sourceTree = ""; }; EA985C3F2970939A00F2FF2E /* TileletModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletModel.swift; sourceTree = ""; }; EA985C5F2970A3F000F2FF2E /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EA985C632970A40E00F2FF2E /* VDSTypographyTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTypographyTokens.xcframework; path = ../SharedFrameworks/VDSTypographyTokens.xcframework; sourceTree = ""; }; EA985C842981AA9C00F2FF2E /* VDS-Enums+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VDS-Enums+Codable.swift"; sourceTree = ""; }; EA985C862981AB0F00F2FF2E /* VDS-Tilelet+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VDS-Tilelet+Codable.swift"; sourceTree = ""; }; EA985C882981AB7100F2FF2E /* VDS-TextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VDS-TextStyle.swift"; sourceTree = ""; }; @@ -2142,10 +2139,7 @@ isa = PBXGroup; children = ( EAD715A92BBC8FAF00DEDA6A /* VDSTokens.xcframework */, - EA985C632970A40E00F2FF2E /* VDSTypographyTokens.xcframework */, EA985C5F2970A3F000F2FF2E /* VDS.framework */, - 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */, - AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */, D29DF0E521E4F3C7003B2FB9 /* MVMCore.framework */, ); name = Frameworks; From 037a52d3792683dbc87a3cc77e50ad30b3d49253 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 13:40:01 -0500 Subject: [PATCH 64/64] fixed the vds color breaking changes for monarch Signed-off-by: Matt Bruce --- .../Atomic/Atoms/Views/TileletModel.swift | 6 ++-- .../Atomic/Extensions/VDS-Enums+Codable.swift | 30 ++++++++----------- .../Extensions/VDS-Tilelet+Codable.swift | 12 +++++--- .../LockUps/TitleLockupModel.swift | 6 ++-- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index 383c03c2..cf5d382b 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -58,7 +58,7 @@ open class TileletModel: TileContainerBaseModel, Molec textPercentage = try container.decodeIfPresent(CGFloat.self, forKey: .textPercentage) if let color = eyebrow?.textColor?.uiColor { - self.eyebrowColor = .custom(color, color) + self.eyebrowColor = .custom(color) } else if let eyebrowColor = try? container.decodeIfPresent(TitleLockup.TextColor.self, forKey: .eyebrowColor) { self.eyebrowColor = eyebrowColor @@ -68,7 +68,7 @@ open class TileletModel: TileContainerBaseModel, Molec } if let color = title?.textColor?.uiColor { - self.titleColor = .custom(color, color) + self.titleColor = .custom(color) } else if let titleColor = try? container.decodeIfPresent(TitleLockup.TitleTextColor.self, forKey: .titleColor) { self.titleColor = titleColor @@ -78,7 +78,7 @@ open class TileletModel: TileContainerBaseModel, Molec } if let color = subTitle?.textColor?.uiColor { - self.subTitleColor = .custom(color, color) + self.subTitleColor = .custom(color) } else if let subTitleColor = try? container.decodeIfPresent(TitleLockup.TextColor.self, forKey: .subTitleColor) { self.subTitleColor = subTitleColor diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index 8bb75a80..68343a10 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -197,8 +197,7 @@ extension VDS.TitleLockup.TextColor: Codable { enum CodingKeys: String, CodingKey { case type - case lightColor - case darkColor + case color } enum CustomColorType: String, Codable { @@ -217,9 +216,8 @@ extension VDS.TitleLockup.TextColor: Codable { case .secondary: self = .secondary case .custom: - let lightColor = try container.decode(Color.self, forKey: .lightColor) - let darkColor = try container.decode(Color.self, forKey: .darkColor) - self = .custom(lightColor.uiColor, darkColor.uiColor) + let color = try container.decode(Color.self, forKey: .color) + self = .custom(color.uiColor) } } @@ -230,11 +228,10 @@ extension VDS.TitleLockup.TextColor: Codable { try container.encode(CustomColorType.primary.rawValue, forKey: .type) case .secondary: try container.encode(CustomColorType.secondary.rawValue, forKey: .type) - case .custom(let lightColor, let darkColor): + case .custom(let color): try container.encode(CustomColorType.custom.rawValue, forKey: .type) - try container.encode(Color(uiColor: lightColor), forKey: .lightColor) - try container.encode(Color(uiColor: darkColor), forKey: .darkColor) - @unknown default: + try container.encode(Color(uiColor: color), forKey: .color) + default: break } } @@ -244,8 +241,7 @@ extension VDS.TitleLockup.TitleTextColor: Codable { enum CodingKeys: String, CodingKey { case type - case lightColor - case darkColor + case color } enum CustomColorType: String, Codable { @@ -261,9 +257,8 @@ extension VDS.TitleLockup.TitleTextColor: Codable { case .primary: self = .primary case .custom: - let lightColor = try container.decode(Color.self, forKey: .lightColor) - let darkColor = try container.decode(Color.self, forKey: .darkColor) - self = .custom(lightColor.uiColor, darkColor.uiColor) + let color = try container.decode(Color.self, forKey: .color) + self = .custom(color.uiColor) } } @@ -272,11 +267,10 @@ extension VDS.TitleLockup.TitleTextColor: Codable { switch self { case .primary: try container.encode(CustomColorType.primary.rawValue, forKey: .type) - case .custom(let lightColor, let darkColor): + case .custom(let color): try container.encode(CustomColorType.custom.rawValue, forKey: .type) - try container.encode(Color(uiColor: lightColor), forKey: .lightColor) - try container.encode(Color(uiColor: darkColor), forKey: .darkColor) - @unknown default: + try container.encode(Color(uiColor: color), forKey: .color) + default: break } } diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift index 9d874675..be231f3e 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Tilelet+Codable.swift @@ -46,7 +46,7 @@ extension Tilelet.DescriptiveIcon: Codable { let accessibleText = try container.decodeIfPresent(String.self, forKey: .accessibleText) if let uiColor = color?.uiColor { self.init(name: name, - colorConfiguration: .init(uiColor, uiColor), + iconColor: .custom(uiColor), size: size, accessibleText: accessibleText) } else { @@ -61,7 +61,9 @@ extension Tilelet.DescriptiveIcon: Codable { try container.encode(name, forKey: .name) try container.encode(size, forKey: .size) try container.encodeIfPresent(accessibleText, forKey: .accessibleText) - try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) + if let color = iconColor?.uiColor { + try container.encode(Color(uiColor: color), forKey: .color) + } } } @@ -81,7 +83,7 @@ extension Tilelet.DirectionalIcon: Codable { let accessibleText = try container.decodeIfPresent(String.self, forKey: .accessibleText) if let uiColor = color?.uiColor { self.init(iconType: iconType, - colorConfiguration: .init(uiColor, uiColor), + iconColor: .custom(uiColor), size: size, accessibleText: accessibleText) } else { @@ -96,6 +98,8 @@ extension Tilelet.DirectionalIcon: Codable { try container.encode(iconType, forKey: .name) try container.encode(size, forKey: .size) try container.encodeIfPresent(accessibleText, forKey: .accessibleText) - try container.encodeIfPresent(colorConfiguration.lightColor.hexString, forKey: .color) + if let color = iconColor?.uiColor { + try container.encode(Color(uiColor: color), forKey: .color) + } } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 3112cc2e..00a53f93 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -99,7 +99,7 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle) if let color = eyebrow?.textColor?.uiColor { - self.eyebrowColor = .custom(color, color) + self.eyebrowColor = .custom(color) } else if let eyebrowColor = try? typeContainer.decodeIfPresent(TitleLockup.TextColor.self, forKey: .eyebrowColor) { self.eyebrowColor = eyebrowColor @@ -109,7 +109,7 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { } if let color = title.textColor?.uiColor { - self.titleColor = .custom(color, color) + self.titleColor = .custom(color) } else if let titleColor = try? typeContainer.decodeIfPresent(TitleLockup.TitleTextColor.self, forKey: .titleColor) { self.titleColor = titleColor @@ -119,7 +119,7 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { } if let color = subTitle?.textColor?.uiColor { - self.subTitleColor = .custom(color, color) + self.subTitleColor = .custom(color) } else if let subTitleColor = try? typeContainer.decodeIfPresent(TitleLockup.TextColor.self, forKey: .subTitleColor) { self.subTitleColor = subTitleColor