Merge branch 'develop' into feature/monarch

This commit is contained in:
Scott Pfeil 2024-05-30 10:09:40 -04:00
commit 25b7af0a07
115 changed files with 4874 additions and 443 deletions

View File

@ -170,8 +170,15 @@
52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; };
5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; }; 5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; };
5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.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 */; }; 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; };
58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.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 */; }; 608211282AC6B57E00C3FC39 /* MVMCoreUILoggingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */; };
8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; }; 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; };
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.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 */; }; FD99130028E21E4900542CC3 /* RuleNotEqualsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */; };
/* End PBXBuildFile section */ /* 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 */ /* Begin PBXFileReference section */
01004F2F22721C3800991ECC /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; }; 01004F2F22721C3800991ECC /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
0103B84D23D7E33A009C315C /* HeadlineBodyToggleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyToggleModel.swift; sourceTree = "<group>"; }; 0103B84D23D7E33A009C315C /* HeadlineBodyToggleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyToggleModel.swift; sourceTree = "<group>"; };
@ -775,10 +792,17 @@
52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = "<group>"; }; 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = "<group>"; };
582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; }; 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; };
5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = "<group>"; }; 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = "<group>"; };
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 = "<group>"; };
583335622BF6509C001D90D7 /* UAD_page_model.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model.json; sourceTree = "<group>"; };
583335642BF6A5C3001D90D7 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UAD_page_model_2.json; sourceTree = "<group>"; };
5833356C2BFBF51C001D90D7 /* UAD_page_model_3.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model_3.json; sourceTree = "<group>"; };
5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = "<group>"; }; 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = "<group>"; };
5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = "<group>"; }; 5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = "<group>"; };
5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = "<group>"; }; 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = "<group>"; };
58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceableMoleculeBehaviorModel.swift; sourceTree = "<group>"; }; 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceableMoleculeBehaviorModel.swift; sourceTree = "<group>"; };
58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeComparisonProtocol.swift; sourceTree = "<group>"; };
608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingHandler.swift; sourceTree = "<group>"; }; 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingHandler.swift; sourceTree = "<group>"; };
8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = "<group>"; }; 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = "<group>"; };
8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = "<group>"; }; 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = "<group>"; };
@ -1220,6 +1244,14 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
583335532BF64E77001D90D7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5833355A2BF64E77001D90D7 /* MVMCoreUI.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D29DF0C921E404D4003B2FB9 /* Frameworks */ = { D29DF0C921E404D4003B2FB9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -1253,6 +1285,7 @@
D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */, D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */,
27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */, 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */,
27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */, 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */,
58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */,
); );
path = ModelProtocols; path = ModelProtocols;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1503,6 +1536,34 @@
path = Accessibility; path = Accessibility;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
583335572BF64E77001D90D7 /* MVMCoreUITests */ = {
isa = PBXGroup;
children = (
583335602BF65063001D90D7 /* JSON */,
583335582BF64E77001D90D7 /* MVMCoreUITests.swift */,
583335642BF6A5C3001D90D7 /* TestUtils.swift */,
);
path = MVMCoreUITests;
sourceTree = "<group>";
};
583335602BF65063001D90D7 /* JSON */ = {
isa = PBXGroup;
children = (
583335612BF6506C001D90D7 /* Modelling */,
);
path = JSON;
sourceTree = "<group>";
};
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 = "<group>";
};
8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = { 8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2012,6 +2073,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */, D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */,
583335572BF64E77001D90D7 /* MVMCoreUITests */,
D29DF0CD21E404D4003B2FB9 /* Products */, D29DF0CD21E404D4003B2FB9 /* Products */,
D29DF0E421E4F3C7003B2FB9 /* Frameworks */, D29DF0E421E4F3C7003B2FB9 /* Frameworks */,
); );
@ -2021,6 +2083,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */, D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */,
583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2597,6 +2660,24 @@
/* End PBXHeadersBuildPhase section */ /* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget 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 */ = { D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = D29DF0D421E404D4003B2FB9 /* Build configuration list for PBXNativeTarget "MVMCoreUI" */; buildConfigurationList = D29DF0D421E404D4003B2FB9 /* Build configuration list for PBXNativeTarget "MVMCoreUI" */;
@ -2621,9 +2702,13 @@
D29DF0C321E404D4003B2FB9 /* Project object */ = { D29DF0C321E404D4003B2FB9 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1320; LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "Verizon Wireless"; ORGANIZATIONNAME = "Verizon Wireless";
TargetAttributes = { TargetAttributes = {
583335552BF64E77001D90D7 = {
CreatedOnToolsVersion = 15.4;
};
D29DF0CB21E404D4003B2FB9 = { D29DF0CB21E404D4003B2FB9 = {
CreatedOnToolsVersion = 10.1; CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1010; LastSwiftMigration = 1010;
@ -2646,11 +2731,22 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */, D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */,
583335552BF64E77001D90D7 /* MVMCoreUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase 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 */ = { D29DF0CA21E404D4003B2FB9 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -2669,6 +2765,15 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase 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 */ = { D29DF0C821E404D4003B2FB9 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -3009,6 +3114,7 @@
EA1758482BC97ED800A5C0D9 /* BadgeIndicator.swift in Sources */, EA1758482BC97ED800A5C0D9 /* BadgeIndicator.swift in Sources */,
012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */,
0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */, 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */,
58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */,
D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */, D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */,
AF1C336928859778006B1001 /* ActionAlertHandler.swift in Sources */, AF1C336928859778006B1001 /* ActionAlertHandler.swift in Sources */,
9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */, 9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */,
@ -3240,6 +3346,14 @@
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
5833355C2BF64E77001D90D7 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */;
targetProxy = 5833355B2BF64E77001D90D7 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
D29DF32821EE8736003B2FB9 /* Localizable.strings */ = { D29DF32821EE8736003B2FB9 /* Localizable.strings */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
@ -3254,6 +3368,49 @@
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration 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 */ = { D29DF0D221E404D4003B2FB9 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */; baseConfigurationReference = 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */;
@ -3446,6 +3603,15 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList 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" */ = { D29DF0C621E404D4003B2FB9 /* Build configuration list for PBXProject "MVMCoreUI" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D29DF0CB21E404D4003B2FB9"
BuildableName = "MVMCoreUI.framework"
BlueprintName = "MVMCoreUI"
ReferencedContainer = "container:MVMCoreUI.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D29DF0CB21E404D4003B2FB9"
BuildableName = "MVMCoreUI.framework"
BlueprintName = "MVMCoreUI"
ReferencedContainer = "container:MVMCoreUI.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -25,4 +25,11 @@ public struct ActionAlertModel: ActionModelProtocol {
public init(alert: AlertModel) { public init(alert: AlertModel) {
self.alert = alert 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
}
} }

View File

@ -20,4 +20,12 @@ public struct ActionCollapseNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData 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
//}
} }

View File

@ -20,4 +20,12 @@ public struct ActionDismissNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData 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
//}
} }

View File

@ -29,4 +29,12 @@ public struct ActionOpenPanelModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData 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
}
} }

View File

@ -22,4 +22,11 @@ public struct ActionTopNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData 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
}
} }

View File

@ -9,7 +9,8 @@
import UIKit import UIKit
import MVMCore import MVMCore
public struct AlertButtonModel: Codable { public struct AlertButtonModel: Codable, Equatable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -62,9 +63,16 @@ public struct AlertButtonModel: Codable {
try container.encodeModel(action, forKey: .action) try container.encodeModel(action, forKey: .action)
try container.encodeIfPresent(preferred, forKey: .preferred) 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 // MARK: - Properties
@ -95,7 +103,6 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol {
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Init // MARK: - Init
//-------------------------------------------------- //--------------------------------------------------
@ -149,6 +156,14 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol {
try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encodeIfPresent(analyticsData, forKey: .analyticsData)
try container.encode(id, forKey: .id) 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 { public extension AlertButtonModel {

View File

@ -79,4 +79,16 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol {
try container.encodeIfPresent(childWidthValue, forKey: .childWidthValue) try container.encodeIfPresent(childWidthValue, forKey: .childWidthValue)
try container.encodeIfPresent(childWidthPercentage, forKey: .childWidthPercentage) 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
}
} }

View File

@ -186,4 +186,34 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits)
try container.encodeIfPresent(disabledAccessibilityTraits, forKey: .disabledAccessibilityTraits) 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
}
} }

View File

@ -33,7 +33,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
[image].compactMap({$0}) [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) return try replaceChildMolecule(at: &image, with: molecule)
} }

View File

@ -99,6 +99,30 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode
try container.encodeIfPresent(size, forKey: .size) try container.encodeIfPresent(size, forKey: .size)
try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) 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 { extension LinkModel {

View File

@ -125,5 +125,19 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol {
try container.encode(width, forKey: .width) try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height) try container.encode(height, forKey: .height)
try container.encode(enabled, forKey: .enabled) 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
} }
} }

View File

@ -21,6 +21,8 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
public var id: String = UUID().uuidString public var id: String = UUID().uuidString
public var backgroundColor: Color? public var backgroundColor: Color?
public var moleculeName: String? public var moleculeName: String?
// Assigned and computed by parent.
public var numberOfPages: Int = 0 public var numberOfPages: Int = 0
/// Sets the current Index to focus on. /// Sets the current Index to focus on.
@ -49,7 +51,6 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
case moleculeName case moleculeName
case backgroundColor case backgroundColor
case currentIndex case currentIndex
case numberOfPages
case alwaysSendAction case alwaysSendAction
case animated case animated
case hidesForSinglePage case hidesForSinglePage
@ -118,7 +119,6 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
try container.encode(id, forKey: .id) try container.encode(id, forKey: .id)
try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encode(numberOfPages, forKey: .numberOfPages)
try container.encode(currentIndex, forKey: .currentIndex) try container.encode(currentIndex, forKey: .currentIndex)
try container.encode(alwaysSendAction, forKey: .alwaysSendAction) try container.encode(alwaysSendAction, forKey: .alwaysSendAction)
try container.encode(animated, forKey: .animated) try container.encode(animated, forKey: .animated)
@ -130,4 +130,31 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
try container.encode(indicatorColor, forKey: .indicatorColor) try container.encode(indicatorColor, forKey: .indicatorColor)
try container.encodeIfPresent(position, forKey: .position) 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
}
} }

View File

@ -61,4 +61,20 @@
case shouldMaskRecordedView case shouldMaskRecordedView
case allowServerParameters 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
}
} }

View File

@ -48,4 +48,9 @@ open class LabelAttributeActionModel: LabelAttributeModel {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeModel(action, forKey: .action) 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)
}
} }

View File

@ -43,4 +43,9 @@
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(textColor, forKey: .textColor) 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
}
} }

View File

@ -55,4 +55,11 @@
try container.encodeIfPresent(name, forKey: .name) try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(size, forKey: .size) 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
}
} }

View File

@ -69,4 +69,12 @@ class LabelAttributeImageModel: LabelAttributeModel {
try container.encodeIfPresent(URL, forKey: .URL) try container.encodeIfPresent(URL, forKey: .URL)
try container.encodeIfPresent(tintColor, forKey: .tintColor) 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
}
} }

View File

@ -7,7 +7,7 @@
// //
@objcMembers open class LabelAttributeModel: ModelProtocol { @objcMembers open class LabelAttributeModel: ModelProtocol, ModelComparisonProtocol, MoleculeModelComparisonProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -75,4 +75,16 @@
try container.encode(location, forKey: .location) try container.encode(location, forKey: .location)
try container.encode(length, forKey: .length) 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
}
} }

View File

@ -66,6 +66,13 @@ import UIKit
try container.encode(style, forKey: .style) try container.encode(style, forKey: .style)
try container.encodeIfPresent(pattern, forKey: .pattern) 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 { public enum UnderlineStyle: String, Codable {

View File

@ -132,6 +132,45 @@ import VDS
try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView)
try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) 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 { extension LabelModel {

View File

@ -129,4 +129,12 @@ public class LineModel: MoleculeModelProtocol, Invertable {
try container.encodeIfPresent(frequency, forKey: .frequency) try container.encodeIfPresent(frequency, forKey: .frequency)
try container.encode(orientation == .vertical, forKey: .useVerticalLine) 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
}
} }

View File

@ -136,4 +136,8 @@ open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{
extension TileContainer: MVMCoreUIViewConstrainingProtocol { extension TileContainer: MVMCoreUIViewConstrainingProtocol {
public func horizontalAlignment() -> UIStackView.Alignment { .leading } public func horizontalAlignment() -> UIStackView.Alignment { .leading }
public func isClippable() -> Bool {
return false
}
} }

View File

@ -25,7 +25,7 @@ open class TileContainerModel: TileContainerBaseModel<TileContainer.Padding, Til
return [molecule] 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) return try replaceChildMolecule(at: &self.molecule, with: molecule)
} }

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -21,9 +21,13 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMol
[titleLockup, buttons] [titleLockup, buttons]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &buttons, with: molecule) if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -24,13 +24,17 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol
[headline, headline2, subHeadline, body, link, buttons] [headline, headline2, subHeadline, body, link, buttons]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &headline, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &headline2, with: molecule) if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &subHeadline, with: molecule) || replaceChildMolecule(at: &headline2, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body, with: molecule) || replaceChildMolecule(at: &subHeadline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &link, with: molecule) || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &buttons, with: molecule) || replaceChildMolecule(at: &link, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -19,7 +19,7 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
[titleLockup] [titleLockup]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) return try replaceChildMolecule(at: &titleLockup, with: molecule)
} }

View File

@ -23,9 +23,13 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMo
[titleLockup, buttons] [titleLockup, buttons]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &buttons, with: molecule) if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &buttons, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -20,9 +20,13 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, Parent
[titleLockup, caretLink] [titleLockup, caretLink]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &caretLink, with: molecule) if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &caretLink, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { public class HeadersH2LinkModel: HeaderModel, ParentMoleculeModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -22,9 +22,13 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMolec
[titleLockup, link] [titleLockup, link]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &link, with: molecule) if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &link, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -22,7 +22,7 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
[titleLockup] [titleLockup]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) return try replaceChildMolecule(at: &titleLockup, with: molecule)
} }

View File

@ -25,15 +25,18 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P
[headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0}) [headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0})
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &headline, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &body, with: molecule) if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &subBody, with: molecule) || replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body2, with: molecule) || replaceChildMolecule(at: &subBody, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body2, with: molecule) || replaceChildMolecule(at: &body2, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &subBody2, with: molecule) || replaceChildMolecule(at: &subBody2, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body3, with: molecule) || replaceChildMolecule(at: &body3, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &subBody3, with: molecule) || replaceChildMolecule(at: &subBody3, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -23,9 +23,13 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, Paren
[titleLockup, button] [titleLockup, button]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &titleLockup, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &button, with: molecule) if try replaceChildMolecule(at: &titleLockup, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &button, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -20,9 +20,13 @@ open class ListLeftVariableCheckboxBodyTextModel: ListItemModel, MoleculeModelPr
[checkbox, headlineBody] [checkbox, headlineBody]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &checkbox, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &headlineBody, with: molecule) if try replaceChildMolecule(at: &checkbox, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &headlineBody, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -20,9 +20,13 @@ public class ListLeftVariableIconAllTextLinksModel: ListItemModel, MoleculeModel
return [image, eyebrowHeadlineBodyLink] return [image, eyebrowHeadlineBodyLink]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &image, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretAllTextLinksModel: ListItemModel,
return [image, eyebrowHeadlineBodyLink, rightLabel] return [image, eyebrowHeadlineBodyLink, rightLabel]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &image, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule) || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//----------------------------------------------------- //-----------------------------------------------------

View File

@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretBodyTextModel: ListItemModel, Par
[image, headlineBody, rightLabel] [image, headlineBody, rightLabel]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &image, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &headlineBody, with: molecule) if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule) || replaceChildMolecule(at: &headlineBody, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//----------------------------------------------------- //-----------------------------------------------------

View File

@ -21,10 +21,14 @@ public class ListLeftVariableIconWithRightCaretModel: ListItemModel, ParentMolec
return [image, leftLabel, rightLabel] return [image, leftLabel, rightLabel]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &image, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &leftLabel, with: molecule) if try replaceChildMolecule(at: &image, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule) || replaceChildMolecule(at: &leftLabel, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//----------------------------------------------------- //-----------------------------------------------------

View File

@ -20,9 +20,13 @@ open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, ParentMolecu
[radioButton, headlineBody] [radioButton, headlineBody]
} }
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &radioButton, with: replacementMolecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &headlineBody, with: replacementMolecule) if try replaceChildMolecule(at: &radioButton, with: replacementMolecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &headlineBody, with: replacementMolecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//----------------------------------------------------- //-----------------------------------------------------

View File

@ -39,7 +39,7 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod
return [headlineBody] 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) return try replaceChildMolecule(at: &headlineBody, with: molecule)
} }

View File

@ -40,9 +40,13 @@ public class ListRightVariableButtonAllTextAndLinksModel: ListItemModel, Molecul
return [button, eyebrowHeadlineBodyLink] return [button, eyebrowHeadlineBodyLink]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &button, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) if try replaceChildMolecule(at: &button, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -20,9 +20,13 @@ public class ListRightVariableRightCaretAllTextAndLinksModel: ListItemModel, Par
[rightLabel, eyebrowHeadlineBodyLink] [rightLabel, eyebrowHeadlineBodyLink]
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &rightLabel, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) if try replaceChildMolecule(at: &rightLabel, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//----------------------------------------------------- //-----------------------------------------------------

View File

@ -9,7 +9,7 @@
import VDSCoreTokens import VDSCoreTokens
import VDS import VDS
public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { public class TitleLockupModel: ParentMoleculeModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
@ -36,10 +36,26 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco
[eyebrow, title, subTitle].compactMap { (molecule: MoleculeModelProtocol?) in molecule } [eyebrow, title, subTitle].compactMap { (molecule: MoleculeModelProtocol?) in molecule }
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &eyebrow, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &title, with: molecule) if try replaceChildMolecule(at: &eyebrow, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &subTitle, with: molecule) || 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)
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -22,9 +22,13 @@ public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, Mo
[headline, body].compactMap({$0}) [headline, body].compactMap({$0})
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &headline, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &body, with: molecule) if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -22,9 +22,13 @@ public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, Mo
[headline, body].compactMap({$0}) [headline, body].compactMap({$0})
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &headline, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &body, with: molecule) if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -22,9 +22,13 @@ public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, Mol
[headline, body].compactMap({$0}) [headline, body].compactMap({$0})
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &headline, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &body, with: molecule) if try replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -68,4 +68,9 @@
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(line, forKey: .line) 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)
}
} }

View File

@ -106,9 +106,30 @@ open class TabBarModel: MoleculeModelProtocol {
try container.encode(selectedTab, forKey: .selectedTab) try container.encode(selectedTab, forKey: .selectedTab)
try container.encodeIfPresent(style, forKey: .style) 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 title: String?
open var image: String open var image: String
open var action: ActionModelProtocol open var action: ActionModelProtocol
@ -142,4 +163,18 @@ open class TabBarItemModel: Codable {
try container.encodeModel(action, forKey: .action) try container.encodeModel(action, forKey: .action)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) 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
}
} }

View File

@ -105,11 +105,39 @@ open class TabsModel: MoleculeModelProtocol {
try container.encode(borderLine, forKey: .borderLine) try container.encode(borderLine, forKey: .borderLine)
try container.encodeIfPresent(minWidth, forKey: .minWidth) 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, Equatable, MoleculeModelComparisonProtocol {
open class TabItemModel: Codable {
open var label: LabelModel open var label: LabelModel
open var action: ActionModelProtocol? open var action: ActionModelProtocol?
public var analyticsData: JSONValueDictionary? public var analyticsData: JSONValueDictionary?
@ -150,4 +178,14 @@ open class TabItemModel: Codable {
try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(action, forKey: .action)
try container.encodeIfPresent(analyticsData, forKey: .analyticsData) 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)
}
} }

View File

@ -23,9 +23,13 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol {
public var children: [MoleculeModelProtocol] { [primaryButton, secondaryButton].compactMap { $0 } } public var children: [MoleculeModelProtocol] { [primaryButton, secondaryButton].compactMap { $0 } }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &primaryButton, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &secondaryButton, with: molecule) 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(secondaryButton, forKey: .secondaryButton)
try container.encodeIfPresent(fillContainer, forKey: .fillContainer) 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)
}
} }

View File

@ -80,6 +80,14 @@ class AccordionListItemModel: MoleculeListItemModel {
try container.encodeModelIfPresent(expandAction, forKey: .expandAction) try container.encodeModelIfPresent(expandAction, forKey: .expandAction)
try container.encodeModelIfPresent(collapseAction, forKey: .collapseAction) 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 { extension AccordionListItemModel: PageBehaviorProtocolRequirer {

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import VDS
open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol {
@ -17,12 +18,14 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol {
open override func addMolecule(_ molecule: MoleculeViewProtocol) { open override func addMolecule(_ molecule: MoleculeViewProtocol) {
super.addMolecule(molecule) super.addMolecule(molecule)
clipsToBounds = (molecule as? MVMCoreUIViewConstrainingProtocol)?.isClippable?() ?? true
contentView.sendSubviewToBack(molecule) contentView.sendSubviewToBack(molecule)
} }
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
clipsToBounds = false
// Covers the card when peaking. // Covers the card when peaking.
peakingCover.backgroundColor = .white peakingCover.backgroundColor = .white

View File

@ -85,8 +85,27 @@
try container.encodeIfPresent(peakingUI, forKey: .peakingUI) try container.encodeIfPresent(peakingUI, forKey: .peakingUI)
try container.encodeIfPresent(peakingArrowColor, forKey: .peakingArrowColor) try container.encodeIfPresent(peakingArrowColor, forKey: .peakingArrowColor)
try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encodeIfPresent(analyticsData, forKey: .analyticsData)
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
try container.encode(enabled, forKey: .enabled) try container.encode(enabled, forKey: .enabled)
try container.encode(readOnly, forKey: .readOnly) 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
}
} }

View File

@ -8,7 +8,8 @@
// A base class that has common list item boilerplate model stuffs. // A base class that has common list item boilerplate model stuffs.
import MVMCore import MVMCore
@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol { @objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol, MoleculeModelComparisonProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -22,6 +23,7 @@ import MVMCore
public var accessibilityTraits: UIAccessibilityTraits? public var accessibilityTraits: UIAccessibilityTraits?
public var accessibilityValue: String? public var accessibilityValue: String?
public var accessibilityText: String? public var accessibilityText: String?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Keys // MARK: - Keys
//-------------------------------------------------- //--------------------------------------------------
@ -128,4 +130,29 @@ import MVMCore
try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue) try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) 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)
}
} }

View File

@ -73,4 +73,15 @@
try container.encodeIfPresent(border, forKey: .border) try container.encodeIfPresent(border, forKey: .border)
try super.encode(to: encoder) 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
}
} }

View File

@ -58,4 +58,11 @@
try container.encodeIfPresent(percent, forKey: .percent) try container.encodeIfPresent(percent, forKey: .percent)
try container.encode(gone, forKey: .gone) 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
}
} }

View File

@ -30,7 +30,7 @@ import UIKit
} }
public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { 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<MVMCoreErrorObject?>?) -> [String]? { public override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {

View File

@ -36,4 +36,12 @@
required public init(from decoder: Decoder) throws { required public init(from decoder: Decoder) throws {
fatalError("init(from:) has not been implemented") 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
}
} }

View File

@ -20,20 +20,21 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol {
private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]? private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]?
public var children: [MoleculeModelProtocol] { public var children: [MoleculeModelProtocol] {
return molecules.flatMap { $0 } return [tabs] + molecules.flatMap { $0 }
} }
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return false } guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return nil }
for (tabIndex, _) in molecules.enumerated() { for (tabIndex, _) in molecules.enumerated() {
for (elementIndex, _) in molecules[tabIndex].enumerated() { for (elementIndex, _) in molecules[tabIndex].enumerated() {
if molecules[tabIndex][elementIndex].id == replacementMolecule.id { if molecules[tabIndex][elementIndex].id == replacementMolecule.id {
let replacedMolecule = molecules[tabIndex][elementIndex]
molecules[tabIndex][elementIndex] = replacementMolecule 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.encode(tabs, forKey: .tabs)
try container.encodeModels2D(molecules, forKey: .molecules) 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 { extension TabsListItemModel: PageBehaviorProtocolRequirer {

View File

@ -23,12 +23,16 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol {
[molecule, topLeftLabel, topRightLabel, bottomLeftLabel, bottomRightLabel].compactMap { $0 } [molecule, topLeftLabel, topRightLabel, bottomLeftLabel, bottomRightLabel].compactMap { $0 }
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &self.molecule, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &topLeftLabel, with: molecule) if try replaceChildMolecule(at: &self.molecule, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &topRightLabel, with: molecule) || replaceChildMolecule(at: &topLeftLabel, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &bottomLeftLabel, with: molecule) || replaceChildMolecule(at: &topRightLabel, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &bottomRightLabel, with: molecule) || replaceChildMolecule(at: &bottomLeftLabel, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &bottomRightLabel, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
public init(with molecule: MoleculeModelProtocol?) { public init(with molecule: MoleculeModelProtocol?) {

View File

@ -73,6 +73,25 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule
try container.encodeIfPresent(imageRenderingMode, forKey: .imageRenderingMode) 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 // MARK: - Method
//-------------------------------------------------- //--------------------------------------------------

View File

@ -129,6 +129,24 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc
try container.encodeIfPresent(style, forKey: .style) try container.encodeIfPresent(style, forKey: .style)
try container.encodeIfPresent(titleOffset, forKey: .titleOffset) 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 { extension NavigationItemModel: ParentMoleculeModelProtocol {

View File

@ -20,7 +20,7 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco
return [molecule] 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) 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.encodeModel(molecule, forKey: .molecule)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) 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)
}
} }

View File

@ -19,7 +19,7 @@ public extension MoleculeContainerModelProtocol {
} }
public extension MoleculeContainerModelProtocol where Self: AnyObject { 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) return try replaceChildMolecule(at: &molecule, with: replacementMolecule)
} }
} }

View File

@ -7,7 +7,7 @@
// //
public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { public class EyebrowHeadlineBodyLinkModel: ParentMoleculeModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -25,11 +25,15 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule
[eyebrow, headline, body, link].compactMap { (molecule: MoleculeModelProtocol?) in molecule } [eyebrow, headline, body, link].compactMap { (molecule: MoleculeModelProtocol?) in molecule }
} }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &eyebrow, with: molecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at: &headline, with: molecule) if try replaceChildMolecule(at: &eyebrow, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &body, with: molecule) || replaceChildMolecule(at: &headline, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &link, with: molecule) || 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(body, forKey: .body)
try container.encodeIfPresent(link, forKey: .link) 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)
}
} }

View File

@ -24,9 +24,13 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol {
[headline, body].compactMap { $0 } [headline, body].compactMap { $0 }
} }
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at:&headline, with: replacementMolecule) var replacedMolecule: MoleculeModelProtocol?
|| replaceChildMolecule(at:&body, with: replacementMolecule) 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(style, forKey: .style)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) 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 { public extension HeadlineBodyModel {

View File

@ -166,9 +166,26 @@ open class Carousel: View {
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.delegateObject = delegateObject self.delegateObject = delegateObject
let originalModel = self.model as? CarouselModel
super.set(with: model, delegateObject, additionalData) super.set(with: model, delegateObject, additionalData)
guard let carouselModel = model as? CarouselModel else { return } 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 accessibilityLabel = carouselModel.accessibilityText
collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor
collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0 collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0

View File

@ -171,6 +171,48 @@ import UIKit
try container.encode(enabled, forKey: .enabled) try container.encode(enabled, forKey: .enabled)
try container.encode(readOnly, forKey: .readOnly) 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 { extension CarouselModel {
@ -179,8 +221,16 @@ extension CarouselModel {
return molecules 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) 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" } )"
}
}

View File

@ -79,4 +79,11 @@
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) 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
}
} }

View File

@ -21,7 +21,7 @@ extension StackModelProtocol {
extension StackModelProtocol where Self: AnyObject { 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) return try replaceChildMolecule(in: &molecules, with: molecule)
} }
} }

View File

@ -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)
}
}

View File

@ -5,7 +5,7 @@ public enum MolecularError: Swift.Error {
case countImbalance(String) case countImbalance(String)
} }
public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol { public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol, MoleculeModelComparisonProtocol, CustomDebugStringConvertible {
var moleculeName: String { get } var moleculeName: String { get }
var backgroundColor: Color? { get set } var backgroundColor: Color? { get set }
var id: String { get } var id: String { get }
@ -18,6 +18,16 @@ public extension MoleculeModelProtocol {
static var categoryName: String { "\(MoleculeModelProtocol.self)" } static var categoryName: String { "\(MoleculeModelProtocol.self)" }
static var categoryCodingKey: String { "moleculeName" } 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. // Helpers made due to swift not able to reconcile which category.

View File

@ -8,48 +8,61 @@
import Foundation import Foundation
public protocol ParentModelProtocol: MoleculeTreeTraversalProtocol { public protocol ParentModelProtocol: ModelProtocol, MoleculeTreeTraversalProtocol {
/// Returns the direct children of this component. (Does not recurse.) /// Returns the direct children of this component. (Does not recurse.)
var children: [MoleculeModelProtocol] { get } var children: [MoleculeModelProtocol] { get }
/// Method for replacing surface level children. (Does not recurse.) /// 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 { public extension ParentModelProtocol where Self: AnyObject {
/// Top level test to replace child molecules. Each parent molecule should attempt to replace. /// 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<T>(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. /// Helper function for replacing a single molecules with type and optionality checks.
func replaceChildMolecule<T>(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol) throws -> Bool { func replaceChildMolecule<T>(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol, replaced: inout MoleculeModelProtocol?) throws -> Bool {
guard let childIdMolecule = childMolecule as? MoleculeModelProtocol else { return false } guard let childIdMolecule = childMolecule as? MoleculeModelProtocol else { return false }
if childIdMolecule.id == replacementMolecule.id { 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))") 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 true
} }
return false return false
} }
func replaceChildMolecule<T>(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". /// Helper for replacing a molecule in place within an array. Note the "in".
func replaceChildMolecule<T>(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol) throws -> Bool { func replaceChildMolecule<T>(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol, replaced: inout MoleculeModelProtocol?) throws -> Bool {
guard let moleculeIdModels = molecules as? [MoleculeModelProtocol], let matchingIndex = moleculeIdModels.firstIndex(where: { guard let moleculeIdModels = molecules as? [MoleculeModelProtocol],
$0.id == replacementMolecule.id let matchingIndex = moleculeIdModels.firstIndex(where: {
}) else { return false } $0.id == replacementMolecule.id
guard let replacementMolecule = replacementMolecule as? T else { })
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))") 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 return true
} }
} }
public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelProtocol { public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelProtocol {
} }
public extension ParentMoleculeModelProtocol { 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. // 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 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) { if (options == .childFirst) {
result = nextPartialResult(result, self, depth) 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. // 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) additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept)
} else { } else {
child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept) onVisit(depth, child, &shouldStop)
} }
guard !shouldStop else { return } guard !shouldStop else { return }
} }
@ -98,3 +111,83 @@ public extension ParentMoleculeModelProtocol {
// if options == .leafOnly don't call on self. // 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<T>(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)
}
}

View File

@ -7,9 +7,11 @@
// //
public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol { public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol, MoleculeModelComparisonProtocol {
var template: String { get } var template: String { get }
var rootMolecules: [MoleculeModelProtocol] { get } var rootMolecules: [MoleculeModelProtocol] { get }
/// Page rendering ID. Unique betwen JSON parses.
var id: String { get }
} }
public extension TemplateModelProtocol { public extension TemplateModelProtocol {
@ -42,27 +44,27 @@ public extension TemplateModelProtocol {
extension TemplateModelProtocol { extension TemplateModelProtocol {
/// Recursively finds and replaces the first child matching the replacement molecule id property. /// 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. // Attempt root level replacement on the template model first.
if try replaceChildMolecule(with: replacementMolecule) { if let replacedMolecule = try replaceChildMolecule(with: replacementMolecule) {
return true return replacedMolecule
} }
var didReplaceMolecule = false var replacedMolecule: MoleculeModelProtocol?
var possibleError: Error? var possibleError: Error?
// Dive into each root thereafter. // Dive into each root thereafter.
depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in
guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return }
do { do {
didReplaceMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) replacedMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule)
} catch { } catch {
possibleError = error possibleError = error
} }
stop = didReplaceMolecule || possibleError != nil stop = replacedMolecule != nil || possibleError != nil
} }
if let error = possibleError { if let error = possibleError {
throw error throw error
} }
return didReplaceMolecule return replacedMolecule
} }
} }

View File

@ -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. /// 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 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 { extension MoleculeDelegateProtocol {

View File

@ -38,7 +38,7 @@ public extension MoleculeTreeTraversalProtocol {
func printMolecules(options: TreeTraversalOptions = .parentFirst) { func printMolecules(options: TreeTraversalOptions = .parentFirst) {
depthFirstTraverse(options: options, depth: 1) { depth, molecule, stop in 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)]")
} }
} }

View File

@ -35,19 +35,26 @@ public extension TemplateProtocol {
public extension TemplateProtocol where Self: PageBehaviorHandlerProtocol, Self: MVMCoreViewControllerProtocol { 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. /// Helper function to do common parsing logic.
func parseTemplate(json: [AnyHashable: Any]?) throws { func parseTemplate(pageJSON: [AnyHashable: Any]) throws -> TemplateModelProtocol {
guard let pageJSON = json else { return }
let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject
let data = try JSONSerialization.data(withJSONObject: pageJSON) let data = try JSONSerialization.data(withJSONObject: pageJSON)
let decoder = JSONDecoder.create(with: delegateObject) 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. // Add additional required behavior models to the template if applicable.
guard var pageBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else { return } guard var templateBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else {
return templateModel
}
templateBehaviorsModel.traverseAndAddRequiredBehaviors()
pageBehaviorsModel.traverseAndAddRequiredBehaviors() return templateModel
var behaviorHandler = self
behaviorHandler.applyBehaviors(pageBehaviorModel: pageBehaviorsModel, delegateObject: delegateObject)
} }
} }

View File

@ -10,11 +10,14 @@ import Foundation
@objcMembers open class BaseTemplateModel: MVMControllerModelProtocol, TabPageModelProtocol { @objcMembers open class BaseTemplateModel: MVMControllerModelProtocol, TabPageModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
open class var identifier: String { "" } open class var identifier: String { "" }
public var id: String = UUID().uuidString
public var pageType: String public var pageType: String
public var template: String { public var template: String {
// Although this is done in the extension, it is needed for the encoding. // 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 hideLeftPanel: Bool?
public var hideRightPanel: Bool? public var hideRightPanel: Bool?
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try replaceChildMolecule(at: &navigationBar, with: molecule) 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.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex)
try container.encode(shouldMaskScreenWhileRecording, forKey: .shouldMaskScreenWhileRecording) 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
}
} }

View File

@ -21,9 +21,8 @@
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Computed Properties // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
open override func parsePageJSON() throws { open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
try parseTemplate(json: loadObject?.pageJSON) return try parseTemplate(loadObject: loadObject)
try super.parsePageJSON()
} }
open override var loadObject: MVMCoreLoadObject? { open override var loadObject: MVMCoreLoadObject? {
@ -80,10 +79,10 @@
} }
open override func handleNewData() { open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
setup() setup()
registerCells() registerCells()
super.handleNewData() super.handleNewData(pageModel)
} }
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
@ -134,7 +133,7 @@
} }
update(cell: cell, size: view.frame.width) 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 // 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 return cell
} }

View File

@ -23,9 +23,14 @@
return super.rootMolecules return super.rootMolecules
} }
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try super.replaceChildMolecule(with: molecule) if let replacedMolecule = try super.replaceChildMolecule(with: molecule) {
|| (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) return replacedMolecule
}
if molecules != nil, let replacedMolecule = try replaceChildMolecule(in: &(molecules!), with: molecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -28,10 +28,16 @@
return super.rootMolecules return super.rootMolecules
} }
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try super.replaceChildMolecule(with: molecule) if let replacedMolecule = try super.replaceChildMolecule(with: molecule) {
|| (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) return replacedMolecule
|| replaceChildMolecule(at: &line, with: molecule) }
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. /// This template requires content.
@ -94,4 +100,14 @@
try container.encodeIfPresent(footerlessSpacerColor, forKey: .footerlessSpacerColor) try container.encodeIfPresent(footerlessSpacerColor, forKey: .footerlessSpacerColor)
try container.encodeIfPresent(footerlessSpacerHeight, forKey: .footerlessSpacerHeight) 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
}
} }

View File

@ -25,8 +25,8 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate {
try decoder.decode(ModalListPageTemplateModel.self, from: data) try decoder.decode(ModalListPageTemplateModel.self, from: data)
} }
override open func handleNewData() { override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
super.handleNewData() super.handleNewData(pageModel)
closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }

View File

@ -23,8 +23,8 @@ open class ModalMoleculeStackTemplate: MoleculeStackTemplate {
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
override open func handleNewData() { override open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
super.handleNewData() super.handleNewData(pageModel)
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in _ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ?? let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ??

View File

@ -46,9 +46,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
// MARK: - Methods // MARK: - Methods
//-------------------------------------------------- //--------------------------------------------------
open override func parsePageJSON() throws { open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
try parseTemplate(json: loadObject?.pageJSON) return try parseTemplate(loadObject: loadObject)
try super.parsePageJSON()
} }
// For subclassing the model. // For subclassing the model.
@ -86,23 +85,31 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
return view return view
} }
open override func handleNewData() { open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
setup() super.handleNewData(pageModel)
registerWithTable()
super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel. if pageModel != nil {
setup()
registerWithTable()
}
} }
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
super.updateUI(for: molecules) 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 }) { if let index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) {
moleculesInfo?[index].molecule = molecule moleculesInfo?[index].molecule = molecule
} }
newData(for: molecule)
}) })
newData(for: molecules)
} }
open override func viewDidAppear(_ animated: Bool) { open override func viewDidAppear(_ animated: Bool) {
@ -169,7 +176,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
} }
(cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) (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 // 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 return cell
} }
@ -233,18 +240,27 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
} }
open func newData(for molecule: MoleculeModelProtocol) { 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. //Check header and footer if replace happens then return.
if updateHeaderFooterView(topView, with: molecule) || molecules.forEach {
updateHeaderFooterView(bottomView, with: molecule, isHeader: false) { if updateHeaderFooterView(topView, with: $0) ||
return updateHeaderFooterView(bottomView, with: $0, isHeader: false) {
return
}
} }
guard let moleculesInfo = moleculesInfo else { return } guard let moleculesInfo = moleculesInfo else { return }
let indicies = moleculesInfo.indices.filter({ index -> Bool in let indicies = moleculesInfo.indices.filter({ index -> Bool in
return moleculesInfo[index].molecule.findFirstMolecule(by: { return moleculesInfo[index].molecule.findFirstMolecule(by: { existingMolecule in
$0.moleculeName == molecule.moleculeName && equal(moleculeA: molecule, moleculeB: $0) molecules.contains { newMolecule in
existingMolecule.moleculeName == newMolecule.moleculeName && equal(moleculeA: existingMolecule, moleculeB: newMolecule)
}
}) != nil }) != nil
}) })
@ -253,7 +269,16 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
let indexPaths = indicies.map { let indexPaths = indicies.map {
return IndexPath(row: $0, section: 0) 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 { if let selectedIndex = selectedIndex {
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none) tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
} }
@ -353,7 +378,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
indexPaths.count > 0 else { return } indexPaths.count > 0 else { return }
tableView?.deleteRows(at: indexPaths, with: animation) tableView?.deleteRows(at: indexPaths, with: animation)
updateViewConstraints() updateViewConstraints()
view.layoutIfNeeded() view.setNeedsLayout()
} }
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) { public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
@ -372,7 +397,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
indexPaths.count > 0 else { return } indexPaths.count > 0 else { return }
self.tableView?.insertRows(at: indexPaths, with: animation) self.tableView?.insertRows(at: indexPaths, with: animation)
self.updateViewConstraints() self.updateViewConstraints()
self.view.layoutIfNeeded() self.view.setNeedsLayout()
} }
public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) { 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() tableView.endUpdates()
self.updateViewConstraints() self.updateViewConstraints()
self.view.layoutIfNeeded() self.view.setNeedsLayout()
} }
public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {

View File

@ -20,10 +20,10 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
open override func handleNewData() { open override func handleNewData(_ pageModel: PageModelProtocol? = nil) {
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
super.handleNewData() super.handleNewData(pageModel)
} }
// For subclassing the model. // For subclassing the model.
@ -31,9 +31,8 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
return try decoder.decode(StackPageTemplateModel.self, from: data) return try decoder.decode(StackPageTemplateModel.self, from: data)
} }
open override func parsePageJSON() throws { open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
try parseTemplate(json: loadObject?.pageJSON) return try parseTemplate(loadObject: loadObject)
try super.parsePageJSON()
} }
open override var loadObject: MVMCoreLoadObject? { open override var loadObject: MVMCoreLoadObject? {

View File

@ -112,7 +112,7 @@ open class SectionListTemplate: MoleculeListTemplate {
(header as? MoleculeViewProtocol)?.set(with: headerInfo.molecule, delegateObjectIVar, nil) (header as? MoleculeViewProtocol)?.set(with: headerInfo.molecule, delegateObjectIVar, nil)
(header as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) (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 // 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 return header
} }
@ -126,7 +126,7 @@ open class SectionListTemplate: MoleculeListTemplate {
(footer as? MoleculeViewProtocol)?.set(with: footerInfo.molecule, delegateObjectIVar, nil) (footer as? MoleculeViewProtocol)?.set(with: footerInfo.molecule, delegateObjectIVar, nil)
(footer as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) (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 // 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 return footer
} }

View File

@ -19,10 +19,14 @@
super.rootMolecules + [moleculeStack] 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) return try super.replaceChildMolecule(with: molecule)
|| replaceChildMolecule(at: &navigationBar, with: molecule) if try replaceChildMolecule(at: &navigationBar, with: molecule, replaced: &replacedMolecule)
|| replaceChildMolecule(at: &moleculeStack, with: molecule) || replaceChildMolecule(at: &moleculeStack, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -21,10 +21,16 @@
[navigationBar, header, footer].compactMap { $0 } [navigationBar, header, footer].compactMap { $0 }
} }
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try super.replaceChildMolecule(with: molecule) if let replacedMolecule = try super.replaceChildMolecule(with: molecule) {
|| replaceChildMolecule(at: &header, with: molecule) return replacedMolecule
|| replaceChildMolecule(at: &footer, with: molecule) }
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.encodeIfPresent(anchorFooter, forKey: .anchorFooter)
try container.encodeModelIfPresent(footer, forKey: .footer) 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)
}
} }

View File

@ -22,9 +22,15 @@
return super.rootMolecules return super.rootMolecules
} }
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? {
return try super.replaceChildMolecule(with: molecule) if let replacedMolecule = try super.replaceChildMolecule(with: molecule) {
|| replaceChildMolecule(at: &middle, with: molecule) return replacedMolecule
}
var replacedMolecule: MoleculeModelProtocol?
if try replaceChildMolecule(at: &middle, with: molecule, replaced: &replacedMolecule) {
return replacedMolecule
}
return nil
} }
//-------------------------------------------------- //--------------------------------------------------

View File

@ -14,9 +14,8 @@ import UIKit
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
open override func parsePageJSON() throws { open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
try parseTemplate(json: loadObject?.pageJSON) return try parseTemplate(loadObject: loadObject)
try super.parsePageJSON()
} }
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {

View File

@ -40,6 +40,7 @@ import MVMCore
public var needsUpdateUI = false public var needsUpdateUI = false
private var observingForResponses: NSObjectProtocol? private var observingForResponses: NSObjectProtocol?
private var initialLoadFinished = false private var initialLoadFinished = false
public var isFirstRender = true
public var previousScreenSize = CGSize.zero public var previousScreenSize = CGSize.zero
public var selectedField: UIView? public var selectedField: UIView?
@ -65,7 +66,9 @@ import MVMCore
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
else { return } 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() { open func stopObservingForResponseJSONUpdates() {
@ -81,14 +84,16 @@ import MVMCore
open func modulesToListenFor() -> [String]? { open func modulesToListenFor() -> [String]? {
let requestModules = loadObject?.requestParameters?.allModules() ?? [] let requestModules = loadObject?.requestParameters?.allModules() ?? []
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor() } ?? [] let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor } ?? []
return requestModules + behaviorModules return requestModules + behaviorModules
} }
@objc open func responseJSONUpdated(notification: Notification) { @objc open func responseJSONUpdated(notification: Notification) {
// Checks for a page we are listening for. // 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), if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
let loadObject,
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
let pageType = page.optionalStringForKey(KeyPageType), let pageType = page.optionalStringForKey(KeyPageType),
@ -97,8 +102,17 @@ import MVMCore
return true return true
}) { }) {
newData = true hasDataUpdate = true
loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) 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. // Checks for modules we are listening for.
@ -106,7 +120,7 @@ import MVMCore
let modulesListened = modulesToListenFor() { let modulesListened = modulesToListenFor() {
for moduleName in modulesListened { for moduleName in modulesListened {
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
newData = true hasDataUpdate = true
var currentModules = loadObject?.modulesJSON ?? [:] var currentModules = loadObject?.modulesJSON ?? [:]
currentModules.updateValue(module, forKey: moduleName) currentModules.updateValue(module, forKey: moduleName)
loadObject?.modulesJSON = currentModules loadObject?.modulesJSON = currentModules
@ -114,21 +128,11 @@ import MVMCore
} }
} }
guard newData else { return } guard hasDataUpdate else { return }
do { MVMCoreDispatchUtility.performBlock(onMainThread: {
// TODO: Parse parsePageJSON modifies the page model on a different thread than self.handleNewData(pageModel)
// 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)
}
}
} }
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool { open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
@ -140,7 +144,12 @@ import MVMCore
// Parse the model for the page. // Parse the model for the page.
do { 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 { } catch let parsingError {
// Log all parsing errors and fail load. // Log all parsing errors and fail load.
if let errorObject = MVMCoreLoadHandler.sharedGlobal()?.error(for: loadObject, causedBy: parsingError) { 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)" return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)"
} }
open func parsePageJSON() throws { open func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier { throw MVMCoreError.error(code: ErrorCode.parsingJSON.rawValue, messageToDisplay: "Template needs to define its model!", messageToLog: "Template needs to define its model!")
MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil))
}
} }
open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool { open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
@ -218,38 +225,85 @@ import MVMCore
return navigationModel 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 @MainActor
open func handleNewData() { open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
if model?.navigationBar == 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() let navigationItem = createDefaultLegacyNavigationModel()
model?.navigationBar = navigationItem newPageModel.navigationBar = navigationItem
} }
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in // Make the template available for onPageNew behavior handling. See if we can have behaviors rely on roots later.
behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar) self.pageModel = newPageModel
// Run through behavior tranformations.
var behaviorUpdatedModels = [MoleculeModelProtocol]()
if var newTemplateModel = newPageModel as? TemplateModelProtocol {
behaviorUpdatedModels = runBehaviorTransformations(on: &newTemplateModel)
} }
if formValidator == nil { // Apply the form validator to the controller.
let rules = model?.formRules if formValidator == nil { // TODO: Can't change form rules?
let rules = (newPageModel as? FormHolderModelProtocol)?.formRules
formValidator = FormValidator(rules) 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. // Notify the manager of new data.
// Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI. // Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI.
manager?.newDataReceived?(in: self) 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. /// Applies the latest model to the UI.
open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
guard molecules == nil else { return }
isFirstRender = false
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in 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 { if let backgroundColor = model?.backgroundColor {
view.backgroundColor = backgroundColor.uiColor view.backgroundColor = backgroundColor.uiColor
} }
@ -258,6 +312,31 @@ import MVMCore
view.setNeedsLayout() 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? { public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? {
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
behavior.willSetupMolecule(with: model, updating: nil) behavior.willSetupMolecule(with: model, updating: nil)
@ -310,7 +389,7 @@ import MVMCore
super.viewDidLoad() super.viewDidLoad()
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)") debugLog("View Controller Loaded")
// We use our own margins. // We use our own margins.
viewRespectsSystemMinimumLayoutMargins = false viewRespectsSystemMinimumLayoutMargins = false
@ -324,7 +403,7 @@ import MVMCore
initialLoad() initialLoad()
} }
handleNewData() handleNewData(pageModel) // Set outside shouldFinishProcessingLoad.
} }
open override func viewDidLayoutSubviews() { open override func viewDidLayoutSubviews() {
@ -393,7 +472,7 @@ import MVMCore
deinit { deinit {
stopObservingForResponseJSONUpdates() stopObservingForResponseJSONUpdates()
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)") debugLog("Deallocated")
} }
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
@ -511,41 +590,6 @@ import MVMCore
// Needed otherwise when subclassed, the extension gets called. // Needed otherwise when subclassed, the extension gets called.
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } 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 // MARK: - MVMCoreUIDetailViewProtocol
//-------------------------------------------------- //--------------------------------------------------
@ -658,3 +702,15 @@ import MVMCore
} }
} }
} }
extension ViewController: CoreLogging {
public var loggingPrefix: String {
"\(self) \(pageType ?? ""): "
}
public static var loggingCategory: String? {
return "Rendering"
}
}

View File

@ -159,6 +159,13 @@ public class AddMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) 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 { public class RemoveMoleculesActionModel: ActionModelProtocol {
@ -186,6 +193,13 @@ public class RemoveMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) 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 { public class SwapMoleculesActionModel: ActionModelProtocol {
@ -213,4 +227,11 @@ public class SwapMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) 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
}
} }

View File

@ -20,6 +20,12 @@ public class PageGetContactBehaviorModel: PageBehaviorModelProtocol {
public var shouldAllowMultipleInstances: Bool { false } public var shouldAllowMultipleInstances: Bool { false }
public init() { } 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 { public class PageGetContactBehavior: PageVisibilityBehavior {

View File

@ -15,6 +15,12 @@ public class GetNotificationAuthStatusBehaviorModel: PageBehaviorModelProtocol {
public var shouldAllowMultipleInstances: Bool { false } public var shouldAllowMultipleInstances: Bool { false }
public init() { } 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 { public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior {

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import MVMCore import MVMCore
import Combine
public class PollingBehaviorModel: PageBehaviorModelProtocol { public class PollingBehaviorModel: PageBehaviorModelProtocol {
public class var identifier: String { "pollingBehavior" } public class var identifier: String { "pollingBehavior" }
@ -45,20 +46,38 @@ public class PollingBehaviorModel: PageBehaviorModelProtocol {
try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad) try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad)
try container.encode(refreshOnShown, forKey: .refreshOnShown) 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 model: PollingBehaviorModel
var delegateObject: MVMCoreUIDelegateObject? 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 pollTimer: DispatchSourceTimer?
var backgroundEventSubscripiton: AnyCancellable?
var remainingTimeToRefresh: TimeInterval { 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 { var refreshOnShown: Bool {
if model.refreshOnFirstLoad && firstTimeLoad { if model.refreshOnFirstLoad && firstTimeLoad {
@ -71,6 +90,15 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior {
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
self.model = model as! PollingBehaviorModel self.model = model as! PollingBehaviorModel
self.delegateObject = delegateObject 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?) { public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
@ -79,26 +107,47 @@ public class PollingBehavior: NSObject, PageVisibilityBehavior {
public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) { public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) {
pollTimer?.cancel() pollTimer?.cancel()
backgroundEventSubscripiton = nil
} }
func resumePollingTimer(withRemainingTime timeRemaining: TimeInterval, refreshAction: ActionModelProtocol, interval: TimeInterval) { func resumePollingTimer(withRemainingTime timeRemaining: TimeInterval, refreshAction: ActionModelProtocol, interval: TimeInterval) {
let delegateObject = delegateObject let delegateObject = delegateObject
pollTimer?.cancel() pollTimer?.cancel()
let pollingId = UUID().uuidString
debugLog("Scheduling timed event \(pollingId) in \(timeRemaining), interval: \(interval)")
pollTimer = DispatchSource.makeTimerSource() pollTimer = DispatchSource.makeTimerSource()
pollTimer?.schedule(deadline: .now() + timeRemaining, repeating: interval) pollTimer?.schedule(deadline: .now() + timeRemaining, repeating: interval)
pollTimer?.setEventHandler(qos:.utility) { pollTimer?.setEventHandler(qos:.utility) { [weak self] in
Task { 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 { if let delegateActionHandler = delegateObject?.actionDelegate as? ActionDelegateProtocol {
try? await delegateActionHandler.performAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) try? await delegateActionHandler.performAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject)
} else { } else {
try? await MVMCoreActionHandler.shared()?.handleAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) try? await MVMCoreActionHandler.shared()?.handleAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject)
} }
self?.debugLog("Finished timed event \(pollingId)")
} }
} }
pollTimer?.resume() 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 { deinit {
debugLog("deinit")
pollTimer?.cancel() pollTimer?.cancel()
} }
} }

View File

@ -33,6 +33,7 @@ public extension PageBehaviorHandlerProtocol {
mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
// Pull the existing behaviors. // Pull the existing behaviors.
var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates } var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates }
// Create and append any new behaviors based on the incoming models. // Create and append any new behaviors based on the incoming models.
let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject) let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject)
behaviors.append(contentsOf: newBehaviors) behaviors.append(contentsOf: newBehaviors)

View File

@ -33,4 +33,9 @@ public extension PageBehaviorModelProtocol {
static var categoryName: String { static var categoryName: String {
"\(PageBehaviorModelProtocol.self)" "\(PageBehaviorModelProtocol.self)"
} }
func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return behaviorName == model.behaviorName
}
} }

Some files were not shown because too many files have changed in this diff Show More