diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index b925a196..c99cd174 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -170,8 +170,15 @@ 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 */; }; + 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 */; }; 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 */; }; @@ -608,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 = ""; }; @@ -775,10 +792,17 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -1220,6 +1244,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; @@ -1253,6 +1285,7 @@ D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */, 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */, 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */, + 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */, ); path = ModelProtocols; sourceTree = ""; @@ -1503,6 +1536,34 @@ 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 */, + 5833356C2BFBF51C001D90D7 /* UAD_page_model_3.json */, + ); + path = Modelling; + sourceTree = ""; + }; 8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = { isa = PBXGroup; children = ( @@ -2012,6 +2073,7 @@ isa = PBXGroup; children = ( D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */, + 583335572BF64E77001D90D7 /* MVMCoreUITests */, D29DF0CD21E404D4003B2FB9 /* Products */, D29DF0E421E4F3C7003B2FB9 /* Frameworks */, ); @@ -2021,6 +2083,7 @@ isa = PBXGroup; children = ( D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */, + 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */, ); name = Products; sourceTree = ""; @@ -2597,6 +2660,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" */; @@ -2621,9 +2702,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; @@ -2646,11 +2731,22 @@ 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 */, + 5833356D2BFBF51C001D90D7 /* UAD_page_model_3.json in Resources */, + 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0CA21E404D4003B2FB9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2669,6 +2765,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; @@ -3009,6 +3114,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 */, @@ -3240,6 +3346,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; @@ -3254,6 +3368,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 */; @@ -3446,6 +3603,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/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..ea2f171c 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.message + && lhs.preferredStyle == rhs.preferredStyle + && lhs.buttonModels == rhs.buttonModels + && lhs.analyticsData == rhs.analyticsData + } } public extension AlertButtonModel { 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 0bfeff61..85195649 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -186,4 +186,34 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) try container.encodeIfPresent(disabledAccessibilityTraits, forKey: .disabledAccessibilityTraits) } + + public func isEqual(to model: any ModelComparisonProtocol) -> 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 + && groupName == model.groupName + && width == model.width + && action.isEqual(to: model.action) + } + + 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/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..a8a80629 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -99,6 +99,30 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode try container.encodeIfPresent(size, forKey: .size) try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } + + public func isEqual(to model: any ModelComparisonProtocol) -> 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) + } + + 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 + } } extension LinkModel { 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/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 28fa22b0..d7381d64 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) @@ -130,4 +130,31 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro try container.encode(indicatorColor, forKey: .indicatorColor) try container.encodeIfPresent(position, forKey: .position) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && 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 + } + + 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/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 1ecbe874..a4db3038 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 ModelComparisonProtocol) -> 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..e1c4ccb1 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 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 6a2a1af5..26cde0fc 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 ModelComparisonProtocol) -> 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..2c9fdcd7 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 ModelComparisonProtocol) -> 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..df60e6ad 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 //-------------------------------------------------- @@ -75,4 +75,16 @@ try container.encode(location, forKey: .location) try container.encode(length, forKey: .length) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + 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/LabelAttributeUnderlineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift index f3578418..cf616ef0 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 ModelComparisonProtocol) -> 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..20e9d501 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -132,6 +132,45 @@ import VDS try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) } + + public func isEqual(to model: any ModelComparisonProtocol) -> 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 + && 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 { + 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 + && html == model.html + && hero == model.hero + && makeWholeViewClickable == model.makeWholeViewClickable + && numberOfLines == model.numberOfLines + && accessibilityText == model.accessibilityText + && accessibilityTraits == model.accessibilityTraits + && inverted == inverted + && attributes.isVisuallyEquivalent(to: model.attributes) + } } extension LabelModel { diff --git a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift index 1b2d7ec2..4bdcefe0 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 ModelComparisonProtocol) -> 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/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/TileContainerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift index 649e5316..b8b64f3d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift @@ -25,7 +25,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/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/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/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 b8d5e475..05457cdf 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..1205c55c 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift @@ -25,15 +25,18 @@ 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: &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..c40331c7 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) } 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 ff09fe26..3c523aab 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -9,7 +9,7 @@ import VDSCoreTokens import VDS -public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { +public class TitleLockupModel: ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties @@ -36,10 +36,26 @@ 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 ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return textAlignment == model.textAlignment + && 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) } //-------------------------------------------------- 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/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/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift index f6a48c6e..0a6cd300 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -106,9 +106,30 @@ 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 + && unSelectedColor == model.unSelectedColor + && 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 + && unSelectedColor == model.unSelectedColor + && 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 +163,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 3376312e..072913f6 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 == 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. + && tabs.isVisuallyEquivalent(to: model.tabs) + } } - - -open class TabItemModel: Codable { +open class TabItemModel: Codable, Equatable, MoleculeModelComparisonProtocol { open var label: LabelModel open var action: ActionModelProtocol? public var analyticsData: JSONValueDictionary? @@ -150,4 +178,14 @@ open class TabItemModel: Codable { try container.encodeModelIfPresent(action, forKey: .action) try container.encodeIfPresent(analyticsData, forKey: .analyticsData) } + + 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/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 0ed1db11..5f7b423c 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 } //-------------------------------------------------- @@ -82,4 +86,12 @@ 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.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/CarouselItem.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift index 509c9e61..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,13 +18,15 @@ 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 = false - + // Covers the card when peaking. peakingCover.backgroundColor = .white peakingCover.alpha = 0 diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index 79fe22ab..f7b875c1 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -85,8 +85,27 @@ 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 ModelComparisonProtocol) -> 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 + } + + 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 aea87731..3ac37b49 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -8,7 +8,8 @@ // 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, MoleculeModelComparisonProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -22,6 +23,7 @@ import MVMCore public var accessibilityTraits: UIAccessibilityTraits? public var accessibilityValue: String? public var accessibilityText: String? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- @@ -128,4 +130,29 @@ import MVMCore try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } + + 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 + && gone == model.gone + && accessibilityText == model.accessibilityText + && accessibilityValue == model.accessibilityValue + && accessibilityTraits == model.accessibilityTraits + && line.matchExistence(with: 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.matchExistence(with: model.line) + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 2afda5a4..945b4b49 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -73,4 +73,15 @@ try container.encodeIfPresent(border, forKey: .border) try super.encode(to: encoder) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + 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 == model.border + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift index b5c42984..1f17a3e7 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift @@ -58,4 +58,11 @@ try container.encodeIfPresent(percent, forKey: .percent) try container.encode(gone, forKey: .gone) } + + 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 + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 19e70358..2f2099f6 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -30,7 +30,7 @@ import UIKit } public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - MoleculeContainer.nameForReuse(with: model, delegateObject) + 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 6fa8b6fa..edb31463 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 override func isEqual(to model: any ModelComparisonProtocol) -> 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/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index 1924820b..943f53ad 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -20,20 +20,21 @@ 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 -> 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,11 @@ 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 molecules.count == model.molecules.count + } } extension TabsListItemModel: PageBehaviorProtocolRequirer { 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/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/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 33758545..a07815ce 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -129,6 +129,24 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc try container.encodeIfPresent(style, forKey: .style) try container.encodeIfPresent(titleOffset, forKey: .titleOffset) } + + open 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 + && 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 + } } extension NavigationItemModel: ParentMoleculeModelProtocol { diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index 6860b40a..140435ad 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,14 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco try container.encodeModel(molecule, forKey: .molecule) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + + 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 { + return isEqual(to: model) + } } 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..14008a5d 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,13 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule try container.encodeIfPresent(body, forKey: .body) try container.encodeIfPresent(link, forKey: .link) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && 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 c8c1a633..80893c44 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,12 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(style, forKey: .style) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return style == style + && backgroundColor == model.backgroundColor + } } public extension HeadlineBodyModel { diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 914e6427..12399292 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -166,9 +166,26 @@ 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 } + + MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)") + + if #available(iOS 15.0, *) { + if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel) { + // 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) + updateModelIndex() // Ensure the new model indexing matches the old. + pagingView?.currentIndex = pageIndex // 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 976018dd..42c8363c 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -171,6 +171,48 @@ import UIKit try container.encode(enabled, forKey: .enabled) try container.encode(readOnly, forKey: .readOnly) } + + public func isEqual(to model: any ModelComparisonProtocol) -> 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 + && fieldKey == model.fieldKey + && 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 { + 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 + } } extension CarouselModel { @@ -179,8 +221,16 @@ 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) } } + +extension CarouselModel: CustomDebugStringConvertible { + + public var debugDescription: String { + return "\(molecules.count) \(molecules.map { ($0 as? CarouselItemModel)?.molecule.moleculeName ?? "unknown" } )" + } + +} diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index bc254727..bffe460b 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -79,4 +79,11 @@ try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } + 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 + && spacing == model.spacing + && molecules.count == model.molecules.count + } } 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 new file mode 100644 index 00000000..97014092 --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -0,0 +1,72 @@ +// +// MoleculeComparisonProtocol.swift +// MVMCoreUI +// +// Created by Kyle Hedden on 4/29/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation + +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 updated 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) + } +} + +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 current 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 e56840e8..672105ef 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,16 @@ public extension MoleculeModelProtocol { static var categoryName: String { "\(MoleculeModelProtocol.self)" } static var categoryCodingKey: String { "moleculeName" } + + func isEqual(to model: ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeName == model.moleculeName + && backgroundColor == model.backgroundColor + } + + 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..d0e1c13f 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -8,48 +8,61 @@ 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 { @@ -64,7 +77,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 +101,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 +111,83 @@ public extension ParentMoleculeModelProtocol { // if options == .leafOnly don't call on self. } } + +public extension ParentModelProtocol { + + typealias DeepCompareResult = (matched: Bool, mine: ModelProtocol?, theirs: ModelProtocol?) + + typealias ModelPair = (mine: ModelProtocol, theirs: ModelProtocol) + + 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, failing test: (ModelProtocol, ModelProtocol)->Bool, options: TreeTraversalOptions = .childFirst) -> DeepCompareResult { + + 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 (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, failing: test) + guard !result.0 else { return result } + } else { + return (true, mine: myChild, theirs: theirChildren[index]) + } + } else if !test(myChildren[index], theirChildren[index]) { + return (true, mine: myChildren[index], theirs: theirChildren[index]) + } + } + + 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] { + + var allDiffs = [ModelPair]() + + let myChildren = children + let theirChildren = anotherParent.children + guard myChildren.count == theirChildren.count else { return [(self, anotherParent)] } + + 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) + allDiffs.append(contentsOf: childDiffs) + } else if !test(myChildren[index], theirChildren[index]) { + allDiffs.append((myChildren[index], theirChildren[index])) + } + } + + 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/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 296f73f5..b598041b 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -7,9 +7,11 @@ // -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. + var id: String { get } } public extension TemplateModelProtocol { @@ -42,27 +44,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/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/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/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 c42797bc..da57579d 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. @@ -35,8 +38,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 } //-------------------------------------------------- @@ -114,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/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index b242c64a..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) { @@ -134,7 +133,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/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..9af7b41d 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. @@ -94,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/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 b825dc68..5eb23107 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 //-------------------------------------------------- @@ -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,23 +85,31 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol return view } - open override func handleNewData() { - setup() - registerWithTable() - super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel. + open override func handleNewData(_ pageModel: PageModelProtocol? = nil) { + super.handleNewData(pageModel) + + if pageModel != nil { + setup() + registerWithTable() + } } open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false 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) { @@ -169,7 +176,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 } @@ -233,18 +240,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 +269,16 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol let indexPaths = indicies.map { return IndexPath(row: $0, section: 0) } - tableView.reloadRows(at: indexPaths, with: .automatic) + + 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 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. + tableView.reloadRows(at: indexPaths, with: .automatic) + } if let selectedIndex = selectedIndex { tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none) } @@ -348,17 +373,17 @@ 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) updateViewConstraints() - view.layoutIfNeeded() + view.setNeedsLayout() } 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) @@ -367,12 +392,12 @@ 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) 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 +445,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/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/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 } 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..481f8cad 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 } //-------------------------------------------------- @@ -71,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/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/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 452d2962..d2638243 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 + public var isFirstRender = true public var previousScreenSize = CGSize.zero public var selectedField: UIView? @@ -65,7 +66,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() { @@ -81,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), @@ -97,8 +102,17 @@ import MVMCore return true }) { - newData = true - loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) + hasDataUpdate = true + loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) + + // Separate page updates from the module updates to avoid unecessary resets to behaviors and full re-renders. + do { + pageModel = try parsePageJSON(loadObject: loadObject) + } catch { + if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") { + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + } + } } // Checks for modules we are listening for. @@ -106,7 +120,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 @@ -114,21 +128,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 { @@ -140,7 +144,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 // 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)) + } } catch let parsingError { // Log all parsing errors and fail load. if let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, causedBy: parsingError) { @@ -180,10 +189,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 { @@ -218,38 +225,85 @@ 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() { - if model?.navigationBar == nil { + open func handleNewData(_ pageModel: PageModelProtocol? = nil) { + + guard var newPageModel = pageModel ?? self.pageModel else { return } + let originalModel = isFirstRender ? nil : self.pageModel as? MVMControllerModelProtocol + + // 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() - model?.navigationBar = navigationItem + newPageModel.navigationBar = navigationItem } - executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in - behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar) + // 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 { + behaviorUpdatedModels = runBehaviorTransformations(on: &newTemplateModel) } - if formValidator == nil { - let rules = model?.formRules + // 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) } - updateUI() + // Reset after tranformations. + self.pageModel = newPageModel + + // 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 { + // 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 = behaviorUpdatedModels //+ pageUpdatedModels // Notify the manager of new data. // Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI. manager?.newDataReceived?(in: self) + + // 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) + } + } } /// Applies the latest model to the UI. open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { - guard molecules == nil else { return } + + isFirstRender = false 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 } @@ -258,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) @@ -310,7 +389,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 @@ -324,7 +403,7 @@ import MVMCore initialLoad() } - handleNewData() + handleNewData(pageModel) // Set outside shouldFinishProcessingLoad. } open override func viewDidLayoutSubviews() { @@ -393,7 +472,7 @@ import MVMCore deinit { stopObservingForResponseJSONUpdates() - MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)") + debugLog("Deallocated") } open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { @@ -511,41 +590,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 { - let replacedModels:[MoleculeModelProtocol] = moleculeModels.compactMap { model in - guard self.attemptToReplace(with: model) else { - return nil - } - return model - } - if replacedModels.count > 0 { - DispatchQueue.main.sync { - self.updateUI(for: replacedModels) - } - } - completionHandler?(replacedModels) - } - } - - open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> Bool { - guard var templateModel = getTemplateModel() else { return false } - var didReplace = false - do { - didReplace = try templateModel.replaceMolecule(with: replacementModel) - if !didReplace { - 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 didReplace - } - //-------------------------------------------------- // MARK: - MVMCoreUIDetailViewProtocol //-------------------------------------------------- @@ -658,3 +702,15 @@ import MVMCore } } } + +extension ViewController: CoreLogging { + + public var loggingPrefix: String { + "\(self) \(pageType ?? ""): " + } + + public static var loggingCategory: String? { + return "Rendering" + } + +} 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 7a92941f..5f87593b 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" } @@ -45,20 +46,38 @@ 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) + } } -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 + 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 { @@ -71,6 +90,15 @@ 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?) -> [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?) { @@ -79,26 +107,47 @@ 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 { + debugLog("deinit") pollTimer?.cancel() } } 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/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index ae09e0ce..9fa84f17 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 [] } } /** @@ -31,6 +31,7 @@ public extension PageBehaviorProtocol { public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { 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) func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) @@ -42,6 +43,10 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { public extension PageMoleculeTransformationBehavior { // All optional. 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 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 51428234..c03594d4 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -13,41 +13,41 @@ 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 { +public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging { + + public var loggingPrefix: String { + "\(self) \(ObjectIdentifier(self).hashValue) \(moleculeIds.prefix(3)) \(moleculeIds.count > 3 ? "+ \(moleculeIds.count - 3) more" : ""):\n" + } + + public static var loggingCategory: String? { + String(describing: Self.self) + } + 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?) { + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?, changes: inout [MoleculeModelProtocol]) -> [MoleculeModelProtocol]? { self.delegateObject = delegateObject - let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil - if shouldListenForListUpdates { - modulesToListenFor = [] - listenForModuleUpdates() - } else { - modulesToListenFor = moleculeIds - stopListeningForModuleUpdates() - } - let moleculeModels = moleculeIds.compactMap { moleculeId in + let moleculeModels = moleculeIds.compactMap { moleculeId in do { return try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId) } catch { @@ -59,57 +59,68 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { return nil } } - if moleculeModels.count > 0 { - delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels, completionHandler: nil) - } + + return findAndReplace(moleculeModels, in: rootMolecules, changes: &changes) } - 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, using: responseJSONUpdated(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], changes: inout [MoleculeModelProtocol]) -> [any MoleculeModelProtocol]? { + debugLog("attempting to replace \(moleculeModels.map { $0.id }) in \(rootMolecules)") + 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.deepEquals(to: rootMolecule) else { + debugLog("top molecule \(updatedMolecule) is the same as \(rootMolecule). skipping...") + return rootMolecule } - MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) - return nil + debugLog("top replacing \(rootMolecule) with \(updatedMolecule)") + logUpdated(molecule: updatedMolecule) + changeList.append(updatedMolecule) + 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.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 + } + debugLog("deep replacing \(replacedMolecule) with \(newMolecule)") + logUpdated(molecule: newMolecule) + changeList.append(newMolecule) + } + } 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 } - 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")") - #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) } - } + let hasReplacement = !changeList.isEmpty + changes.append(contentsOf: changeList) + debugLog("replacing \(hasReplacement ? updatedRootMolecules.count : 0) molecules for \(changes)") + 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") } } 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/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; diff --git a/MVMCoreUI/Containers/Views/ContainerModel.swift b/MVMCoreUI/Containers/Views/ContainerModel.swift index b54590dd..b9549bf8 100644 --- a/MVMCoreUI/Containers/Views/ContainerModel.swift +++ b/MVMCoreUI/Containers/Views/ContainerModel.swift @@ -7,13 +7,14 @@ // -open class ContainerModel: ContainerModelProtocol, Codable { +open class ContainerModel: ContainerModelProtocol, Codable, ModelComparisonProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- public var id: String = UUID().uuidString + public var hasStableId = false public var horizontalAlignment: UIStackView.Alignment? public var useHorizontalMargins: Bool? @@ -74,11 +75,14 @@ open class ContainerModel: ContainerModelProtocol, Codable { //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- - + 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) @@ -95,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) @@ -109,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 + } } diff --git a/MVMCoreUI/CustomPrimitives/Color.swift b/MVMCoreUI/CustomPrimitives/Color.swift index 678b845c..475e8aea 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 //-------------------------------------------------- @@ -154,3 +155,9 @@ public final class Color: Codable { } } } + +extension Color: Equatable { + public static func == (lhs: Color, rhs: Color) -> Bool { + return lhs.hex == rhs.hex + } +} 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/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift index ceca433b..d5e0182e 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) 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 + } } 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 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/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 new file mode 100644 index 00000000..5cbcb62a --- /dev/null +++ b/MVMCoreUITests/MVMCoreUITests.swift @@ -0,0 +1,216 @@ +// +// 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 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) + 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)) + + let results3: [MoleculeModelProtocol] = listTemplateModel1.findAllTheirsNotEqual(against: listTemplateModel4) + XCTAssertTrue(results3.count == 2) + } + + 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)) + } + +}