diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index ee3a600c..038ecb6c 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -168,6 +168,11 @@ 526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */; }; 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; }; 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; + 5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; }; + 5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */; }; + 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; }; + 5870636F2ACF238E00CA18D5 /* ReadableDecodingErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */; }; + 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; }; 608211282AC6B57E00C3FC39 /* MVMCoreUILoggingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */; }; 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; }; 71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */; }; @@ -387,7 +392,7 @@ D23A8FEE26122F7D007E14CE /* VisibleBehaviorForVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FED26122F7D007E14CE /* VisibleBehaviorForVideo.swift */; }; D23A8FF82612308D007E14CE /* PageBehaviorProtocolRequirer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */; }; D23A8FFB26123189007E14CE /* PageBehaviorModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */; }; - D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */; }; + D23A90002612347A007E14CE /* PageBehaviorContainerModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */; }; D23A9004261234CE007E14CE /* PageBehaviorHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */; }; D23A900926125FFB007E14CE /* GetContactBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A900826125FFB007E14CE /* GetContactBehavior.swift */; }; D23A90682614B0B4007E14CE /* CoreUIModelMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A90672614B0B4007E14CE /* CoreUIModelMapping.swift */; }; @@ -766,6 +771,11 @@ 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnCompareChanges.swift; sourceTree = ""; }; 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethod.swift; sourceTree = ""; }; 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = ""; }; + 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; + 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = ""; }; + 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = ""; }; + 5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadableDecodingErrors.swift; sourceTree = ""; }; + 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceableMoleculeBehaviorModel.swift; sourceTree = ""; }; 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingHandler.swift; sourceTree = ""; }; 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = ""; }; @@ -985,7 +995,7 @@ D23A8FED26122F7D007E14CE /* VisibleBehaviorForVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleBehaviorForVideo.swift; sourceTree = ""; }; D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorProtocolRequirer.swift; sourceTree = ""; }; D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorModelProtocol.swift; sourceTree = ""; }; - D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorHandlerModelProtocol.swift; sourceTree = ""; }; + D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorContainerModelProtocol.swift; sourceTree = ""; }; D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorHandlerProtocol.swift; sourceTree = ""; }; D23A900826125FFB007E14CE /* GetContactBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetContactBehavior.swift; sourceTree = ""; }; D23A90672614B0B4007E14CE /* CoreUIModelMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUIModelMapping.swift; sourceTree = ""; }; @@ -1224,10 +1234,10 @@ D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */, 014AA72323C501E2006F3E93 /* ContainerModelProtocol.swift */, D23EA7FA2475F09800D60C34 /* CarouselItemProtocol.swift */, + 01EB3683236097C0006832FA /* MoleculeModelProtocol.swift */, 012A88C3238D86E600FE3DA1 /* CarouselItemModelProtocol.swift */, 012A88B0238C880100FE3DA1 /* CarouselPagingModelProtocol.swift */, EA05EFAA278DE53600828819 /* ClearableModelProtocol.swift */, - 01EB3683236097C0006832FA /* MoleculeModelProtocol.swift */, 012A889B23889E8400FE3DA1 /* TemplateModelProtocol.swift */, D28A837823C7D5BC00DFE4FC /* PageModelProtocol.swift */, 011B58EF23A2AA980085F53C /* ListItemModelProtocol.swift */, @@ -1343,7 +1353,7 @@ children = ( D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */, D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */, - D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */, + D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */, D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */, 27F973522466074500CAB5C5 /* PageBehaviorProtocol.swift */, ); @@ -1409,6 +1419,8 @@ 27F973512466071600CAB5C5 /* Behaviors */ = { isa = PBXGroup; children = ( + 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */, + 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */, 0A1C30972620F61A00B47F3B /* Protocols */, 27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */, D23A900826125FFB007E14CE /* GetContactBehavior.swift */, @@ -1475,11 +1487,11 @@ path = OneColumn; sourceTree = ""; }; - 7199C8142A4F3A40001568B7 /* Accessibility */ = { + 582272082B1FC53E00F75BAE /* Accessibility */ = { isa = PBXGroup; children = ( - 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */, - 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */, + 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */, + 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */, ); path = Accessibility; sourceTree = ""; @@ -1596,6 +1608,7 @@ D202AFE2242A5F1400E5BEDF /* Extensions */ = { isa = PBXGroup; children = ( + 5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */, D202AFE3242A5F5E00E5BEDF /* NSTextAlignment+Extension.swift */, 0A209CD223A7E2810068F8B0 /* UIStackViewAlignment+Extension.swift */, D21EE53B23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift */, @@ -2009,7 +2022,7 @@ D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */ = { isa = PBXGroup; children = ( - 7199C8142A4F3A40001568B7 /* Accessibility */, + 582272082B1FC53E00F75BAE /* Accessibility */, 01F2C1FC27C81F9700DC3D36 /* Managers */, D2ED27D8254B0C1F00A1C293 /* Alerts */, 27F973512466071600CAB5C5 /* Behaviors */, @@ -2742,6 +2755,7 @@ 011D95A3240453F8000E3791 /* RuleRegexModel.swift in Sources */, D2E2A98323D8B32D000B42E6 /* EyebrowHeadlineBodyLinkModel.swift in Sources */, 012A88AD238C418100FE3DA1 /* TemplateProtocol.swift in Sources */, + 5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */, BB6C6AC1242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift in Sources */, 32D2609724C19E2100B56344 /* LockupsPlanSMLXLModel.swift in Sources */, EA985C872981AB0F00F2FF2E /* VDS-Tilelet+Codable.swift in Sources */, @@ -2762,7 +2776,7 @@ 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */, D2A92884241ACB25004E01C6 /* ProgrammaticScrollViewController.swift in Sources */, EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */, - D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */, + D23A90002612347A007E14CE /* PageBehaviorContainerModelProtocol.swift in Sources */, EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */, 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */, D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */, @@ -2793,6 +2807,7 @@ D2E2A9A123E095AB000B42E6 /* ButtonModelProtocol.swift in Sources */, AF8118302AB39B0900FAD1BA /* RawRepresentableCodable.swift in Sources */, 94C2D9AB23872EB50006CF46 /* LabelAttributeActionModel.swift in Sources */, + 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */, D22D8395241FB41200D3DF69 /* UIStackView+Extension.swift in Sources */, 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */, 525239C02407BCFF00454969 /* ListTwoColumnPriceDetailsModel.swift in Sources */, @@ -2808,6 +2823,7 @@ 0A0FEC7425D42A5E00AF2548 /* BaseItemPickerEntryField.swift in Sources */, D29DF2A221E7AF4E003B2FB9 /* MVMCoreUIUtility.m in Sources */, D23A8FF82612308D007E14CE /* PageBehaviorProtocolRequirer.swift in Sources */, + 5870636F2ACF238E00CA18D5 /* ReadableDecodingErrors.swift in Sources */, 94C2D9A723872DA90006CF46 /* LabelAttributeColorModel.swift in Sources */, 943820842432382400B43AF3 /* WebView.swift in Sources */, 0103B84E23D7E33A009C315C /* HeadlineBodyToggleModel.swift in Sources */, @@ -2876,7 +2892,6 @@ 0A7ECC702441001C00C828E8 /* UIToolbar+Extension.swift in Sources */, D29DF26D21E6AA0B003B2FB9 /* FLAnimatedImageView.m in Sources */, AA3561AC24C9684400452EB1 /* ListRightVariableRightCaretAllTextAndLinksModel.swift in Sources */, - 7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */, D209234F244F77FD0044AD09 /* ThreeLayerCenterTemplate.swift in Sources */, 525019E52406852100EED91C /* ListFourColumnDataUsageDividerModel.swift in Sources */, 32D2609624C19E2100B56344 /* LockupsPlanSMLXL.swift in Sources */, @@ -2960,6 +2975,7 @@ EA7D81642B2BABCB00D29F9E /* TooltipModel.swift in Sources */, AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */, AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */, + 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */, D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */, D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */, 27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */, @@ -3009,6 +3025,7 @@ 8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */, AFA4932229E5EF2E001A9663 /* NotificationHandler.swift in Sources */, BB2FB3BD247E7EF200DF73CD /* Tags.swift in Sources */, + 5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */, AA104ADC244734EA004D2810 /* HeadersH1LandingPageHeaderModel.swift in Sources */, BBAA4F03243D8E3B005AAD5F /* RadioBoxes.swift in Sources */, 323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */, @@ -3053,7 +3070,6 @@ D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */, D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, - 71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */, AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */, 8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift index f3dc2798..dfce1882 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift @@ -33,6 +33,7 @@ import Foundation super.set(with: model, delegateObject, additionalData) FormValidator.setupValidation(for: castModel, delegate: delegateObject?.formHolderDelegate) + setState() } public func setState() { diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift index 8f4bb522..6d293a34 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift @@ -8,7 +8,7 @@ import Foundation -open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol { +open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -29,6 +29,14 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro public var updateUI: ActionBlock? + public var children: [MoleculeModelProtocol] { + [image].compactMap({$0}) + } + + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &image, with: molecule) + } + public init(image: ImageViewModel?, action: ActionModelProtocol) { self.image = image self.action = action diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 59611756..d69c501d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -368,6 +368,10 @@ public typealias ActionBlock = () -> () imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize) } + if let tintColor = imageAtt.tintColor { + imageAttachment.image = imageAttachment.image?.withTintColor(tintColor.uiColor, renderingMode: .alwaysTemplate) + } + // Confirm that the intended image location is within range. if 0...labelText.count ~= imageAtt.location { let mutableString = NSMutableAttributedString() diff --git a/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift b/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift new file mode 100644 index 00000000..4b48b0a4 --- /dev/null +++ b/MVMCoreUI/Atomic/Extensions/ReadableDecodingErrors.swift @@ -0,0 +1,63 @@ +// +// ReadableDecodingErrors.swift +// MVMCore +// +// Created by Kyle Hedden on 10/5/23. +// Copyright © 2023 myverizon. All rights reserved. +// + +import Foundation + +protocol HumanReadableDecodingErrorProtocol { + var readableDescription: String { get } +} + +extension JSONError: HumanReadableDecodingErrorProtocol { + var readableDescription: String { + switch (self) { + case .other(let other): + if let other = other as? HumanReadableDecodingErrorProtocol { + return other.readableDescription + } + return description + default: + return description + } + } +} + +extension ModelRegistry.Error: HumanReadableDecodingErrorProtocol { + var readableDescription: String { + switch (self) { + case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil: + return "Model identifier \"\(identifier!)\" is not mapped for \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })" + + case .decoderErrorObjectNotPresent(let codingKey, let codingPath): + return "Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })" + + default: + return "Registry error: \((self as NSError).localizedFailureReason ?? self.localizedDescription)" + } + } +} + +extension DecodingError: HumanReadableDecodingErrorProtocol { + var readableDescription: String { + switch (self) { + case .keyNotFound(let codingKey, let context): + return "Required key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })" + + case .valueNotFound(_, let context): + return "Value not found @ \(context.codingPath.map { return $0.stringValue })" + + case .typeMismatch(_, let context): + return "Value type mismatch @ \(context.codingPath.map { return $0.stringValue })" + + case .dataCorrupted(let context): + return "Data corrupted @ \(context.codingPath.map { return $0.stringValue })" + + @unknown default: + return (self as NSError).localizedFailureReason ?? self.localizedDescription + } + } +} diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index 6a9cc598..8f184d5a 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -25,8 +25,6 @@ extension VDS.Tabs.Overflow: Codable {} extension VDS.Tabs.Size: Codable {} extension VDS.TextLink.Size: Codable {} extension VDS.TextLinkCaret.IconPosition: Codable {} -extension VDS.TileContainer.BackgroundColor: Codable {} -extension VDS.TileContainer.Padding: Codable {} extension VDS.TileContainer.AspectRatio: Codable {} extension VDS.Tooltip.FillColor: Codable {} extension VDS.Tooltip.Size: Codable {} @@ -61,3 +59,75 @@ extension DecodableDefault { public typealias BlackColor = Wrapper public typealias Surface = Wrapper } + +extension VDS.TileContainer.BackgroundColor: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .custom(let value): + try container.encode(value) + default: + try container.encode(String(reflecting: self)) + } + } + + // Init from decoder to handle the decoding based on the type + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + switch type { + case "primary": + self = .primary + case "secondary": + self = .secondary + case "white": + self = .white + case "black": + self = .black + default: + self = .custom(type) + } + } +} + +extension VDS.TileContainer.Padding: Codable { + enum PaddingError: Error { + case valueNotFound(type: String) + } + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .custom(let value): + try container.encode(value) + default: + try container.encode(String(reflecting: self)) + } + } + + // Init from decoder to handle the decoding based on the type + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + let type = try container.decode(String.self) + switch type { + case "padding2X": + self = .padding2X + case "padding4X": + self = .padding4X + case "padding6X": + self = .padding6X + case "padding8X": + self = .padding8X + case "padding12X": + self = .padding12X + default: + throw PaddingError.valueNotFound(type: type) + } + } catch PaddingError.valueNotFound(let type) { + throw PaddingError.valueNotFound(type: type) + } catch { + let type = try container.decode(CGFloat.self) + self = .custom(type) + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift index cdb61a4d..9ed584a9 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift @@ -20,6 +20,11 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMol [headlineBody, buttons] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &buttons, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift index 79705fd9..651a7f89 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift @@ -24,6 +24,15 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol [headline, headline2, subHeadline, body, link, buttons] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &headline2, with: molecule) + || replaceChildMolecule(at: &subHeadline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + || replaceChildMolecule(at: &link, with: molecule) + || replaceChildMolecule(at: &buttons, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift index 9d246d98..9f3e6133 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift @@ -20,6 +20,10 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol [headlineBody] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift index 77bc25c1..8a5a3e64 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift @@ -22,6 +22,11 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMo [headlineBody, buttons] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &buttons, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift index e0831a40..c50fb736 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift @@ -19,6 +19,11 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, Parent [headlineBody, caretLink] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &caretLink, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift index 20d6afe2..ced6b47a 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift @@ -21,6 +21,11 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMolec [headlineBody, link] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &link, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift index 86d38707..774102da 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift @@ -21,6 +21,10 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol [headlineBody] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift index ebca7e25..54a62b5b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift @@ -25,6 +25,17 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P [headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0}) } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + || replaceChildMolecule(at: &subBody, with: molecule) + || replaceChildMolecule(at: &body2, with: molecule) + || replaceChildMolecule(at: &body2, with: molecule) + || replaceChildMolecule(at: &subBody2, with: molecule) + || replaceChildMolecule(at: &body3, with: molecule) + || replaceChildMolecule(at: &subBody3, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift index e04ab07f..2a91c860 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift @@ -22,6 +22,11 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, Paren [headlineBody, button] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &button, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift index 3ed92e02..ba7d4986 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableCheckboxBodyTextModel.swift @@ -20,6 +20,11 @@ open class ListLeftVariableCheckboxBodyTextModel: ListItemModel, MoleculeModelPr [checkbox, headlineBody] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &checkbox, with: molecule) + || replaceChildMolecule(at: &headlineBody, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift index 46de07fa..c7a32e78 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconAllTextLinksModel.swift @@ -20,6 +20,11 @@ public class ListLeftVariableIconAllTextLinksModel: ListItemModel, MoleculeModel return [image, eyebrowHeadlineBodyLink] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &image, with: molecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + } + //-------------------------------------------------- // MARK: - Method //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift index a4fed00e..9d5ce50b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinksModel.swift @@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretAllTextLinksModel: ListItemModel, return [image, eyebrowHeadlineBodyLink, rightLabel] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &image, with: molecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + || replaceChildMolecule(at: &rightLabel, with: molecule) + } + //----------------------------------------------------- // MARK: - Methods //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift index 633989d5..6b95c669 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyTextModel.swift @@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretBodyTextModel: ListItemModel, Par [image, headlineBody, rightLabel] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &image, with: molecule) + || replaceChildMolecule(at: &headlineBody, with: molecule) + || replaceChildMolecule(at: &rightLabel, with: molecule) + } + //----------------------------------------------------- // MARK: - Methods //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift index 186ad988..a804b8d4 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretModel.swift @@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretModel: ListItemModel, ParentMolec return [image, leftLabel, rightLabel] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &image, with: molecule) + || replaceChildMolecule(at: &leftLabel, with: molecule) + || replaceChildMolecule(at: &rightLabel, with: molecule) + } + //----------------------------------------------------- // MARK: - Methods //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift index 6d8881e4..808ee19e 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyTextModel.swift @@ -7,7 +7,7 @@ // -open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, MoleculeModelProtocol { +open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, ParentMoleculeModelProtocol { //----------------------------------------------------- // MARK: - Properties //----------------------------------------------------- @@ -15,7 +15,16 @@ open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, MoleculeMode public static var identifier: String = "listLVRBBdy" public var radioButton: RadioButtonModel public var headlineBody: HeadlineBodyModel - + + public var children: [MoleculeModelProtocol] { + [radioButton, headlineBody] + } + + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &radioButton, with: replacementMolecule) + || replaceChildMolecule(at: &headlineBody, with: replacementMolecule) + } + //----------------------------------------------------- // MARK: - Initializer //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift index 94be5a53..aee85f74 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextBodyTextModel.swift @@ -39,6 +39,10 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod return [headlineBody] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headlineBody, with: molecule) + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift index 30856e55..3d68a8ff 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift @@ -40,6 +40,11 @@ public class ListRightVariableButtonAllTextAndLinksModel: ListItemModel, Molecul return [button, eyebrowHeadlineBodyLink] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &button, with: molecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift index e2bc1691..94d65c37 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinksModel.swift @@ -20,6 +20,11 @@ public class ListRightVariableRightCaretAllTextAndLinksModel: ListItemModel, Par [rightLabel, eyebrowHeadlineBodyLink] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &rightLabel, with: molecule) + || replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule) + } + //----------------------------------------------------- // MARK: - Methods //----------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 89d32ca9..bb77c26a 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -64,6 +64,12 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco [eyebrow, title, subTitle].compactMap { (molecule: MoleculeModelProtocol?) in molecule } } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &eyebrow, with: molecule) + || replaceChildMolecule(at: &title, with: molecule) + || replaceChildMolecule(at: &subTitle, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift index 8a039144..af226405 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift @@ -22,6 +22,11 @@ public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, Mo [headline, body].compactMap({$0}) } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift index 6a40ebf7..596b9ff0 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift @@ -22,6 +22,11 @@ public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, Mo [headline, body].compactMap({$0}) } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift index 09b462fe..ea4c3ce2 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift @@ -22,6 +22,11 @@ public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, Mol [headline, body].compactMap({$0}) } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift index 17da8897..2d176986 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift @@ -30,6 +30,7 @@ import VDS if let delegate { onTabDidSelect = { [weak self] index in guard let self else { return } + viewModel.selectedIndex = index delegate.didSelectItem(.init(row: index, section: 0), tabs: self) } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index f43f2265..c2cad75a 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -23,6 +23,11 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { public var children: [MoleculeModelProtocol] { [primaryButton, secondaryButton].compactMap { $0 } } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &primaryButton, with: molecule) + || replaceChildMolecule(at: &secondaryButton, with: molecule) + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index 6413a381..aea87731 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -6,7 +6,7 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // // A base class that has common list item boilerplate model stuffs. - +import MVMCore @objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol { //-------------------------------------------------- @@ -18,6 +18,7 @@ public var hideArrow: Bool? public var line: LineModel? public var style: ListItemStyle? + public var gone: Bool = false public var accessibilityTraits: UIAccessibilityTraits? public var accessibilityValue: String? public var accessibilityText: String? @@ -31,6 +32,7 @@ case hideArrow case line case style + case gone case accessibilityTraits case accessibilityValue case accessibilityText @@ -107,6 +109,7 @@ hideArrow = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideArrow) line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) style = try typeContainer.decodeIfPresent(ListItemStyle.self, forKey: .style) + gone = try typeContainer.decodeIfPresent(Bool.self, forKey: .gone) ?? false accessibilityTraits = try typeContainer.decodeIfPresent(UIAccessibilityTraits.self, forKey: .accessibilityTraits) accessibilityValue = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityValue) try super.init(from: decoder) @@ -120,6 +123,7 @@ try container.encodeIfPresent(hideArrow, forKey: .hideArrow) try container.encodeIfPresent(line, forKey: .line) try container.encodeIfPresent(style, forKey: .style) + try container.encode(gone, forKey: .gone) try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index aca15eba..88bb6db4 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -9,7 +9,7 @@ import UIKit -public class TabsListItemModel: ListItemModel, MoleculeModelProtocol { +public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -19,6 +19,23 @@ public class TabsListItemModel: ListItemModel, MoleculeModelProtocol { var molecules: [[ListItemModelProtocol & MoleculeModelProtocol]] private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]? + public var children: [MoleculeModelProtocol] { + return molecules.flatMap { $0 } + } + + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return false } + for (tabIndex, _) in molecules.enumerated() { + for (elementIndex, _) in molecules[tabIndex].enumerated() { + if molecules[tabIndex][elementIndex].id == replacementMolecule.id { + molecules[tabIndex][elementIndex] = replacementMolecule + return true + } + } + } + return false + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift index dc8f2396..4925d03b 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift @@ -18,10 +18,19 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol { public var bottomLeftLabel: LabelModel? public var bottomRightLabel: LabelModel? public var molecule: MoleculeModelProtocol? + public var children: [MoleculeModelProtocol] { [molecule, topLeftLabel, topRightLabel, bottomLeftLabel, bottomRightLabel].compactMap { $0 } } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &self.molecule, with: molecule) + || replaceChildMolecule(at: &topLeftLabel, with: molecule) + || replaceChildMolecule(at: &topRightLabel, with: molecule) + || replaceChildMolecule(at: &bottomLeftLabel, with: molecule) + || replaceChildMolecule(at: &bottomRightLabel, with: molecule) + } + public init(with molecule: MoleculeModelProtocol?) { self.molecule = molecule } diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMolecule.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMolecule.swift index ce76d379..beadfd1c 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMolecule.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMolecule.swift @@ -23,7 +23,7 @@ open class ModuleMolecule: Container { super.set(with: model, delegateObject, additionalData) guard let moduleMoleculeModel = model as? ModuleMoleculeModel, - let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMoleculeModel.moduleName) else { + let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMoleculeModel.moduleName) else { // Critical error return } @@ -49,7 +49,7 @@ open class ModuleMolecule: Container { public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { guard let moduleMolecule = model as? ModuleMoleculeModel, - let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName), + let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName), let classType = ModelRegistry.getMoleculeClass(moduleModel), let height = classType.estimatedHeight(with: moduleModel, delegateObject) else { // Critical error @@ -60,7 +60,7 @@ open class ModuleMolecule: Container { public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { guard let moduleMolecule = model as? ModuleMoleculeModel, - let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName), + let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName), let classType = ModelRegistry.getMoleculeClass(moduleModel), let name = classType.nameForReuse(with: moduleModel, delegateObject) else { // Critical error @@ -72,7 +72,7 @@ open class ModuleMolecule: Container { public override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { guard let moduleName = (model as? ModuleMoleculeModel)?.moduleName, - let _ = delegateObject?.moleculeDelegate?.getModuleWithName(moduleName) else { + let _ = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleName) else { if let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess), code: CoreUIErrorCode.ErrorCodeModuleMolecule.rawValue, domain: ErrorDomainNative, location: String(describing: self)) { error?.pointee = errorObject MVMCoreUILoggingHandler.shared()?.addError(toLog: errorObject) diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index 735e95c1..6860b40a 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -20,6 +20,10 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco return [molecule] } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &self.molecule, with: molecule) + } + //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift index 84b1f38e..cbda8dde 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerProtocol.swift @@ -12,9 +12,14 @@ public protocol MoleculeContainerModelProtocol: ContainerModelProtocol, ParentMo } public extension MoleculeContainerModelProtocol { - + var children: [MoleculeModelProtocol] { return [molecule] } - +} + +public extension MoleculeContainerModelProtocol where Self: AnyObject { + mutating func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &molecule, with: replacementMolecule) + } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index 0d46916e..90f72be3 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -25,6 +25,13 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule [eyebrow, headline, body, link].compactMap { (molecule: MoleculeModelProtocol?) in molecule } } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &eyebrow, with: molecule) + || replaceChildMolecule(at: &headline, with: molecule) + || replaceChildMolecule(at: &body, with: molecule) + || replaceChildMolecule(at: &link, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index a3eaf918..76e26f77 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -24,13 +24,9 @@ [headline, body].compactMap { $0 } } - public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool { - return [ - \HeadlineBodyModel.headline, - \HeadlineBodyModel.body, - ].contains { - replaceChildMolecule(on: self, keyPath: $0, replacementMolecule: replacementMolecule) - } + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at:&headline, with: replacementMolecule) + || replaceChildMolecule(at:&body, with: replacementMolecule) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 2ad92343..421c2e95 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -181,6 +181,10 @@ open class Carousel: View { } } + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return (model as? CarouselModel)?.height ?? 80 + } + //-------------------------------------------------- // MARK: - JSON Setters //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 04e98287..976018dd 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -179,4 +179,8 @@ extension CarouselModel { return molecules } + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(in: &molecules, with: molecule) + } + } diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index da6c067e..bc254727 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -21,19 +21,6 @@ public var spacing: CGFloat = Padding.Four public var useStackSpacingBeforeFirstItem = false - public var children: [MoleculeModelProtocol] { - return molecules - } - - public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool { - guard let replacementMolecule = replacementMolecule as? StackItemModelProtocol & MoleculeModelProtocol else { return false } - guard let matchingIndex = molecules.firstIndex(where: { molecule in - molecule.id == replacementMolecule.id - }) else { return false } - molecules[matchingIndex] = replacementMolecule - return true - } - //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift b/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift index 068418f0..1456f6e5 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModelProtocol.swift @@ -16,7 +16,12 @@ public protocol StackModelProtocol: ParentMoleculeModelProtocol { } extension StackModelProtocol { - public var children: [MoleculeModelProtocol] { return molecules } - +} + +extension StackModelProtocol where Self: AnyObject { + + public mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(in: &molecules, with: molecule) + } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ListItemModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ListItemModelProtocol.swift index b3b7be7e..74d6e3fe 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ListItemModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ListItemModelProtocol.swift @@ -20,6 +20,7 @@ public protocol ListItemModelProtocol: ContainerModelProtocol, AccessibilityMode var action: ActionModelProtocol? { get set } var hideArrow: Bool? { get set } var style: ListItemStyle? { get set } + var gone: Bool { get set } } // Not a strict requirement. diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index 31adc6ef..e56840e8 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -68,8 +68,14 @@ public extension Array where Element == MoleculeModelProtocol { } func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool) -> Void) { - forEach { (molecule) in - molecule.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit) + var shouldStop = false + let stopIntercept = { depth, molecule, stop in + onVisit(depth, molecule, &shouldStop) + stop = shouldStop + } + for molecule in self { + molecule.depthFirstTraverse(options: options, depth: depth, onVisit: stopIntercept) + if shouldStop { break } } } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 78377a25..ef53b177 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -8,11 +8,48 @@ import Foundation -public protocol ParentMoleculeModelProtocol: MoleculeModelProtocol, AnyObject { +public protocol ParentModelProtocol: MoleculeTreeTraversalProtocol { + /// Returns the direct children of this component. (Does not recurse.) var children: [MoleculeModelProtocol] { get } - func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool + /// Method for replacing surface level children. (Does not recurse.) + mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool +} + +public extension ParentModelProtocol where Self: AnyObject { + + /// Top level test to replace child molecules. Each parent molecule should attempt to replace. + func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { return false } + + /// Helper function for replacing a single molecules with type and optionality checks. + func replaceChildMolecule(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + guard let childIdMolecule = childMolecule as? MoleculeModelProtocol else { return false } + if childIdMolecule.id == replacementMolecule.id { + guard let replacementMolecule = replacementMolecule as? T else { + throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))") + } + childMolecule = replacementMolecule + return true + } + return false + } + + /// Helper for replacing a molecule in place within an array. Note the "in". + func replaceChildMolecule(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + guard let moleculeIdModels = molecules as? [MoleculeModelProtocol], let matchingIndex = moleculeIdModels.firstIndex(where: { + $0.id == replacementMolecule.id + }) else { return false } + guard let replacementMolecule = replacementMolecule as? T else { + throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))") + } + molecules[matchingIndex] = replacementMolecule + return true + } +} + +public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelProtocol { + } public extension ParentMoleculeModelProtocol { @@ -37,43 +74,27 @@ public extension ParentMoleculeModelProtocol { } func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool)->Void) { - var stop = false + var shouldStop = false if (options == .parentFirst) { - onVisit(depth, self, &stop) - guard !stop else { return } + onVisit(depth, self, &shouldStop) + guard !shouldStop else { return } + } + let stopIntercept: (Int, MoleculeModelProtocol, inout Bool)->Void = { depth, model, stop in + onVisit(depth, model, &shouldStop) + stop = shouldStop } for child in children { if let additionalParent = child as? ParentMoleculeModelProtocol { // Safety net to make sure the ParentMoleculeModelProtocol's method extension is called over the base MoleculeModelProtocol. - additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit) + additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept) } else { - child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit) + child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept) } - guard !stop else { return } + guard !shouldStop else { return } } if (options == .childFirst) { - onVisit(depth, self, &stop) + onVisit(depth, self, &shouldStop) } // if options == .leafOnly don't call on self. } - - /// Top level test to replace child molecules. Each parent molecule should attempt to replace. - func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool { return false } - - /// Helper function for replacing molecules on a path. - func replaceChildMolecule(on target: P, keyPath: ReferenceWritableKeyPath, replacementMolecule: MoleculeModelProtocol) -> Bool { - if let currentMolecule = target[keyPath: keyPath], currentMolecule.id == replacementMolecule.id, let newHeadline = replacementMolecule as? T { - target[keyPath: keyPath] = newHeadline - return true - } - return false - } - - func replaceChildMolecule(on target: P, keyPath: ReferenceWritableKeyPath, replacementMolecule: MoleculeModelProtocol) -> Bool { - if target[keyPath: keyPath].id == replacementMolecule.id, let newHeadline = replacementMolecule as? T { - target[keyPath: keyPath] = newHeadline - return true - } - return false - } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 2cd233b0..296f73f5 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -7,13 +7,17 @@ // -public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol { +public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol { var template: String { get } var rootMolecules: [MoleculeModelProtocol] { get } } public extension TemplateModelProtocol { + var children: [MoleculeModelProtocol] { + return rootMolecules + } + var template: String { get { return Self.identifier } } @@ -34,3 +38,31 @@ public extension TemplateModelProtocol { return rootMolecules.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit) } } + +extension TemplateModelProtocol { + + /// Recursively finds and replaces the first child matching the replacement molecule id property. + mutating func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool { + // Attempt root level replacement on the template model first. + if try replaceChildMolecule(with: replacementMolecule) { + return true + } + + var didReplaceMolecule = false + var possibleError: Error? + // Dive into each root thereafter. + depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in + guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } + do { + didReplaceMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule) + } catch { + possibleError = error + } + stop = didReplaceMolecule || possibleError != nil + } + if let error = possibleError { + throw error + } + return didReplaceMolecule + } +} diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift index f2f01b6c..e7a4bdef 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift @@ -9,39 +9,49 @@ public protocol MoleculeDelegateProtocol: AnyObject { + func getTemplateModel() -> TemplateModelProtocol? + func getRootMolecules() -> [MoleculeModelProtocol] - /// returns a module for the corresponding module name. + /// Returns a raw module map for the corresponding module name if the key name exists. func getModuleWithName(_ name: String?) -> [AnyHashable: Any]? - func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? + /// Returns the decoded module for the corresponding module name if the key name exists. Throws if there is a decoding error. + func getModuleWithName(_ moleculeName: String) throws -> MoleculeModelProtocol? /// 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 replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol]) } extension MoleculeDelegateProtocol { + public func getRootMolecules() -> [MoleculeModelProtocol] { + getTemplateModel()?.rootMolecules ?? [] + } + public func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } - public func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? { + public func getModuleWithName(_ moleculeName: String) throws -> MoleculeModelProtocol? { let moduleJSON: [AnyHashable: Any]? = getModuleWithName(moleculeName) - guard let moduleJSON = moduleJSON as? [String: Any], - let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), - let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) - else { return nil } + guard let moduleJSON = moduleJSON as? [String: Any] else { return nil } - do { - return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as? MoleculeModelProtocol - } catch { - MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "error: \(error)") + guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName), + let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else { + throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName)) } - return nil + return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as? MoleculeModelProtocol } } extension MoleculeDelegateProtocol where Self: TemplateProtocol { + + public func getTemplateModel() -> TemplateModelProtocol? { + return templateModel + } + public func getRootMolecules() -> [MoleculeModelProtocol] { templateModel?.rootMolecules ?? [] } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift index a2da7cf4..b030d92a 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift @@ -38,7 +38,7 @@ public extension MoleculeTreeTraversalProtocol { func printMolecules(options: TreeTraversalOptions = .parentFirst) { depthFirstTraverse(options: options, depth: 1) { depth, molecule, stop in - print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule)]") + print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule): \(molecule.id)]") } } @@ -51,10 +51,23 @@ public extension MoleculeTreeTraversalProtocol { } } - func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) { - depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in - guard let parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } - stop = parentMolecule.replaceChildMolecule(with: replacementMolecule) + func filterMoleculeTree(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> [MoleculeModelProtocol] { + return reduceDepthFirstTraverse(options: options, depth: 0, initialResult: []) { (accumulator, molecule, depth) in + if condition(molecule) { + return accumulator + [molecule] + } + return accumulator } } + + func findFirstMolecule(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> MoleculeModelProtocol? { + var foundMolecule: MoleculeModelProtocol? + depthFirstTraverse(options: options, depth: 0) { depth, molecule, isDone in + isDone = condition(molecule) + if isDone { + foundMolecule = molecule + } + } + return foundMolecule + } } diff --git a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift index 144001bc..41c8f56a 100644 --- a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift @@ -28,25 +28,26 @@ public extension TemplateProtocol { } } + func decodeTemplate(using decoder: JSONDecoder, from data: Data) throws -> TemplateModel { + try decoder.decode(TemplateModel.self, from: data) + } +} + +public extension TemplateProtocol where Self: PageBehaviorHandlerProtocol, Self: MVMCoreViewControllerProtocol { + /// Helper function to do common parsing logic. func parseTemplate(json: [AnyHashable: Any]?) throws { guard let pageJSON = json else { return } - let delegateObject = (self as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject + let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject let data = try JSONSerialization.data(withJSONObject: pageJSON) let decoder = JSONDecoder.create(with: delegateObject) templateModel = try decodeTemplate(using: decoder, from: data) // Add additional required behaviors if applicable. - guard var behaviorHandlerModel = templateModel as? TemplateModelProtocol & PageBehaviorHandlerModelProtocol, - var behaviorHandler = self as? PageBehaviorHandlerProtocol else { return } - behaviorHandlerModel.traverseAndAddRequiredBehaviors() - behaviorHandler.createBehaviors(for: behaviorHandlerModel, delegateObject: delegateObject) - if let viewController = self as? UIViewController { - MVMCoreUISession.sharedGlobal()?.applyGlobalBehaviors(to: viewController) - } - } - - func decodeTemplate(using decoder: JSONDecoder, from data: Data) throws -> TemplateModel { - try decoder.decode(TemplateModel.self, from: data) + guard var pageBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else { return } + + pageBehaviorsModel.traverseAndAddRequiredBehaviors() + var behaviorHandler = self + behaviorHandler.applyBehaviors(pageBehaviorModel: pageBehaviorsModel, delegateObject: delegateObject) } } diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index 5a84078e..6c831ac8 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -34,6 +34,11 @@ import Foundation public var shouldMaskScreenWhileRecording: Bool? public var hideLeftPanel: Bool = false public var hideRightPanel: Bool = false + + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &navigationBar, with: molecule) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index 3683e1e9..b242c64a 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -81,11 +81,15 @@ open override func handleNewData() { + setup() + registerCells() + super.handleNewData() + } + + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false - setup() - registerCells() - super.handleNewData() + super.updateUI(for: molecules) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift index fc71921e..e94f8153 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplateModel.swift @@ -22,6 +22,11 @@ } return super.rootMolecules } + + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try super.replaceChildMolecule(with: molecule) + || (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) + } //-------------------------------------------------- // MARK: - Initializer diff --git a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift index 4796bed7..e1713b01 100644 --- a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift @@ -26,6 +26,12 @@ return super.rootMolecules } + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try super.replaceChildMolecule(with: molecule) + || (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule)) + || replaceChildMolecule(at: &line, with: molecule) + } + /// This template requires content. func validateModelHasContent() throws { if header == nil, diff --git a/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift index baf8d78a..a328dac4 100644 --- a/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift @@ -21,8 +21,8 @@ open class ModalSectionListTemplate: SectionListTemplate { // MARK: - Lifecycle //-------------------------------------------------- - override open func handleNewData() { - super.handleNewData() + override open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) _ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in guard let self = self else { return } let closeAction = (self.templateModel as? ModalSectionListTemplateModel)?.closeAction ?? diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 30a9b5c5..c00e5181 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -82,11 +82,22 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } open override func handleNewData() { - topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false - bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false setup() registerWithTable() - super.handleNewData() + super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel. + } + + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false + bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false + super.updateUI(for: molecules) + + molecules?.forEach({ molecule in + if let index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) { + moleculesInfo?[index].molecule = molecule + } + newData(for: molecule) + }) } open override func viewDidAppear(_ animated: Bool) { @@ -124,6 +135,10 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } } + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return (getMoleculeInfo(for: indexPath)?.molecule as? ListItemModelProtocol)?.gone == true ? 0 : UITableView.automaticDimension + } + open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { guard let moleculeInfo = getMoleculeInfo(for: indexPath), let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject) @@ -141,7 +156,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol guard let moleculeInfo = getMoleculeInfo(for: indexPath), let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier) else { return UITableViewCell() } - + cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? ListItemModelProtocol)?.gone == true (cell as? MoleculeViewProtocol)?.reset() (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath) if let moleculeView = cell as? MoleculeViewProtocol { @@ -191,35 +206,43 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } open func newData(for molecule: MoleculeModelProtocol) { - //TODO: expand for header, navigation, etc - guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in - if equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) { - return true - } else if let parent = moleculeInfo.molecule as? ParentMoleculeModelProtocol { - // Get all molecules of the same type for faster check. - let molecules: [MoleculeModelProtocol] = parent.reduceDepthFirstTraverse(options: .childFirst, depth: 0, initialResult: []) { (accumulator, currentMolecule, depth) in - if currentMolecule.moleculeName == molecule.moleculeName { - return accumulator + [currentMolecule] - } - return accumulator - } - for moleculeB in molecules { - if equal(moleculeA: molecule, moleculeB: moleculeB) { - return true - } - } - } - return false - }) else { return } + + //Check header and footer if replace happens then return. + if updateHeaderFooterView(topView, with: molecule) || + updateHeaderFooterView(bottomView, with: molecule, isHeader: false) { + return + } + + guard let moleculesInfo = moleculesInfo else { return } + + let indicies = moleculesInfo.indices.filter({ index -> Bool in + return moleculesInfo[index].molecule.findFirstMolecule(by: { + $0.moleculeName == molecule.moleculeName && equal(moleculeA: molecule, moleculeB: $0) + }) != nil + }) // Refresh the cell. (reload loses cell selection) let selectedIndex = tableView.indexPathForSelectedRow - let indexPath = IndexPath(row: index, section: 0) - tableView.reloadRows(at: [indexPath], with: .automatic) + let indexPaths = indicies.map { + return IndexPath(row: $0, section: 0) + } + tableView.reloadRows(at: indexPaths, with: .automatic) if let selectedIndex = selectedIndex { tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none) } } + + ///Helper functions to update header/footer view + private func updateHeaderFooterView(_ view: UIView?, with molecule: MoleculeModelProtocol, isHeader: Bool = true) -> Bool { + if let updateView = view, + let moleculeView = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [updateView]).first(where: { $0.model?.moleculeName == molecule.moleculeName && $0.model?.id == molecule.id }) { + updateMoleculeView(moleculeView, from: molecule) + //Redraw the header/footer for content update + isHeader ? showHeader(nil) : showFooter(nil) + return true + } + return false + } //-------------------------------------------------- // MARK: - Convenience @@ -293,7 +316,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol } extension MoleculeListTemplate: MoleculeListProtocol { - open func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) { + public func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) { for (index, indexPath) in indexPaths.sorted().enumerated() { let removeIndex = indexPath.row - index moleculesInfo?.remove(at: removeIndex) @@ -306,7 +329,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { view.layoutIfNeeded() } - open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) { + public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) { var indexPaths: [IndexPath] = [] for molecule in molecules { @@ -325,7 +348,7 @@ extension MoleculeListTemplate: MoleculeListProtocol { self.view.layoutIfNeeded() } - open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { + public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) }) else { return nil } diff --git a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift index 1ed663c2..d5d783d3 100644 --- a/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/StackPageTemplateModel.swift @@ -16,7 +16,13 @@ public var moleculeStack: StackModel public override var rootMolecules: [MoleculeModelProtocol] { - [navigationBar, header, moleculeStack, footer].compactMap { $0 } + super.rootMolecules + [moleculeStack] + } + + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try super.replaceChildMolecule(with: molecule) + || replaceChildMolecule(at: &navigationBar, with: molecule) + || replaceChildMolecule(at: &moleculeStack, with: molecule) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift index eb8a674f..993cd9f0 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift @@ -26,8 +26,8 @@ } } - open override func handleNewData() { - super.handleNewData() + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) heightConstraint?.isActive = true } } diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift index 9f130089..6ca05550 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift @@ -21,6 +21,12 @@ [navigationBar, header, footer].compactMap { $0 } } + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try super.replaceChildMolecule(with: molecule) + || replaceChildMolecule(at: &header, with: molecule) + || replaceChildMolecule(at: &footer, with: molecule) + } + //-------------------------------------------------- // MARK: - Init //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift index ab4f29d5..d3acea86 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerPageTemplateModel.swift @@ -22,6 +22,11 @@ return super.rootMolecules } + public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try super.replaceChildMolecule(with: molecule) + || replaceChildMolecule(at: &middle, with: molecule) + } + //-------------------------------------------------- // MARK: - Init //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift index 933d6043..b0e36239 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift @@ -19,10 +19,10 @@ import UIKit try super.parsePageJSON() } - open override func handleNewData() { + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { topViewOutsideOfScroll = templateModel?.anchorHeader ?? false bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false - super.handleNewData() + super.updateUI(for: molecules) } open override func viewForTop() -> UIView? { diff --git a/MVMCoreUI/BaseControllers/MVMControllerModelProtocol.swift b/MVMCoreUI/BaseControllers/MVMControllerModelProtocol.swift index 78f6f604..ce9c132c 100644 --- a/MVMCoreUI/BaseControllers/MVMControllerModelProtocol.swift +++ b/MVMCoreUI/BaseControllers/MVMControllerModelProtocol.swift @@ -9,6 +9,6 @@ import Foundation -public protocol MVMControllerModelProtocol: TemplateModelProtocol, FormHolderModelProtocol, PageBehaviorHandlerModelProtocol { +public protocol MVMControllerModelProtocol: TemplateModelProtocol, FormHolderModelProtocol, PageBehaviorContainerModelProtocol { } diff --git a/MVMCoreUI/BaseControllers/ScrollingViewController.swift b/MVMCoreUI/BaseControllers/ScrollingViewController.swift index 4fd23be2..49bda46b 100644 --- a/MVMCoreUI/BaseControllers/ScrollingViewController.swift +++ b/MVMCoreUI/BaseControllers/ScrollingViewController.swift @@ -63,8 +63,8 @@ open class ScrollingViewController: ViewController { registerForKeyboardNotifications() } - open override func handleNewData() { - super.handleNewData() + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) // will change scrollView indicatorStyle automatically on the basis of backgroundColor var greyScale: CGFloat = 0 if view.backgroundColor?.getWhite(&greyScale, alpha: nil) ?? false { diff --git a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift index 05a575ae..bcce25b0 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift @@ -112,8 +112,8 @@ import Foundation } } - open override func handleNewData() { - super.handleNewData() + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) topView?.removeFromSuperview() bottomView?.removeFromSuperview() topView = viewForTop() diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index bd369276..9ae3b1c4 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -55,8 +55,11 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController, Rotor tableView.reloadData() } - open override func handleNewData() { - super.handleNewData() + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) + + guard molecules == nil else { return } + createViewForTableHeader() createViewForTableFooter() tableView?.reloadData() diff --git a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift index 4560aa09..837d19c7 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerViewController.swift @@ -49,8 +49,8 @@ open class ThreeLayerViewController: ProgrammaticScrollViewController, RotorView } } - open override func handleNewData() { - super.handleNewData() + open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + super.updateUI(for: molecules) // Removes the views topView?.removeFromSuperview() diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index ad554901..08a84d83 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -38,12 +38,19 @@ import MVMCore public var behaviors: [PageBehaviorProtocol]? public var needsUpdateUI = false - private var observingForResponses = false + private var observingForResponses: NSObjectProtocol? private var initialLoadFinished = false public var previousScreenSize = CGSize.zero public var selectedField: UIView? + public var pageUpdateQueue: OperationQueue = { + let pageUpdateQueue = OperationQueue() + pageUpdateQueue.maxConcurrentOperationCount = 1 + pageUpdateQueue.qualityOfService = .userInteractive + return pageUpdateQueue + }() + /// Checks if the screen width has changed open func screenSizeChanged() -> Bool { !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) @@ -54,18 +61,17 @@ import MVMCore //-------------------------------------------------- open func observeForResponseJSONUpdates() { - guard !observingForResponses, + guard observingForResponses == nil, (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) else { return } - observingForResponses = true - NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) + observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:)) } open func stopObservingForResponseJSONUpdates() { - guard observingForResponses else { return } - NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) - observingForResponses = false + guard let observingForResponses = observingForResponses else { return } + NotificationCenter.default.removeObserver(observingForResponses) + self.observingForResponses = nil } open func pagesToListenFor() -> [String]? { @@ -74,7 +80,9 @@ import MVMCore } open func modulesToListenFor() -> [String]? { - loadObject?.requestParameters?.allModules() + let requestModules = loadObject?.requestParameters?.allModules() ?? [] + let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor() } ?? [] + return requestModules + behaviorModules } @objc open func responseJSONUpdated(notification: Notification) { @@ -109,9 +117,12 @@ import MVMCore guard newData else { return } do { + // TODO: Parse parsePageJSON modifies the page model on a different thread than + // the UI update which could cause discrepancies. Parse should return the resulting + // object and assignment should be synchronized on handleNewData(model: ). try parsePageJSON() MVMCoreDispatchUtility.performBlock(onMainThread: { - self.handleNewDataAndUpdateUI() + self.handleNewData() }) } catch { if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") { @@ -158,40 +169,13 @@ import MVMCore } return false } - + return true } func describe(parsingError: Error) -> String { - if let registryError = parsingError as? ModelRegistry.Error { - switch (registryError) { - case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil: - return "Error parsing template. Model identifier \"\(identifier!)\" is not mapped for \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })" - - case .decoderErrorObjectNotPresent(let codingKey, let codingPath): - return "Error parsing template. Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })" - - default: - return "Error parsing template. Registry error: \((registryError as NSError).localizedFailureReason ?? registryError.localizedDescription)" - } - } - if let decodingError = parsingError as? DecodingError { - switch (decodingError) { - case .keyNotFound(let codingKey, let context): - return "Error parsing template. Required key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })" - - case .valueNotFound(_, let context): - return "Error parsing template. Value not found @ \(context.codingPath.map { return $0.stringValue })" - - case .typeMismatch(_, let context): - return "Error parsing template. Value type mismatch @ \(context.codingPath.map { return $0.stringValue })" - - case .dataCorrupted(let context): - return "Error parsing template. Data corrupted @ \(context.codingPath.map { return $0.stringValue })" - - @unknown default: - return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)" - } + if let error = parsingError as? HumanReadableDecodingErrorProtocol { + return "Error parsing template. \(error.readableDescription)" } return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)" } @@ -227,13 +211,6 @@ import MVMCore return true } - /// Calls processNewData and then sets the ui to update with updateView - open func handleNewDataAndUpdateUI() { - handleNewData() - needsUpdateUI = true - view.setNeedsLayout() - } - /// Creates a legacy navigation model. open func createDefaultLegacyNavigationModel() -> NavigationItemModel { let navigationModel = NavigationItemModel() @@ -241,7 +218,8 @@ import MVMCore return navigationModel } - /// Processes any new data. Called after the page is loaded the first time and on response updates for this page, + /// Processes any new data. Called after the page is loaded the first time and on response updates for this page, Triggers a render refresh. + @MainActor open func handleNewData() { if model?.navigationBar == nil { let navigationItem = createDefaultLegacyNavigationModel() @@ -257,12 +235,27 @@ import MVMCore formValidator = FormValidator(rules) } + updateUI() + + // Notify the manager of new data. + // Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI. + manager?.newDataReceived?(in: self) + } + + /// Applies the latest model to the UI. + open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { + guard molecules == nil else { return } + + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + behavior.willRender(rootMolecules: getRootMolecules(), delegateObjectIVar) + } + if let backgroundColor = model?.backgroundColor { view.backgroundColor = backgroundColor.uiColor } - // Notify the manager of new data - manager?.newDataReceived?(in: self) + needsUpdateUI = true + view.setNeedsLayout() } public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? { @@ -331,7 +324,7 @@ import MVMCore initialLoad() } - handleNewDataAndUpdateUI() + handleNewData() } open override func viewDidLayoutSubviews() { @@ -511,13 +504,47 @@ import MVMCore // MARK: - MoleculeDelegateProtocol //-------------------------------------------------- - open func getRootMolecules() -> [MoleculeModelProtocol] { - model?.rootMolecules ?? [] - } + open func getTemplateModel() -> TemplateModelProtocol? { model } + + open func getRootMolecules() -> [MoleculeModelProtocol] { model?.rootMolecules ?? [] } // Needed otherwise when subclassed, the extension gets called. open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } + public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol]) { + 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) + } + } + } + } + + 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 //-------------------------------------------------- diff --git a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift index ed9b8196..6e11e074 100644 --- a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift +++ b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift @@ -76,7 +76,7 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo self.delegate = delegateObject } - public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + public func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { guard let list = delegate?.moleculeListDelegate else { return } for case let model as (MoleculeModelProtocol & ListItemModelProtocol & AddMolecules) in rootMolecules { if let moleculesToAdd = model.getRecursiveMoleculesToAdd(), diff --git a/MVMCoreUI/Behaviors/GetContactBehavior.swift b/MVMCoreUI/Behaviors/GetContactBehavior.swift index 77c0d7f4..6f9a3cd4 100644 --- a/MVMCoreUI/Behaviors/GetContactBehavior.swift +++ b/MVMCoreUI/Behaviors/GetContactBehavior.swift @@ -43,7 +43,7 @@ public class PageGetContactBehavior: PageVisibilityBehavior { MVMCoreDispatchUtility.performBlock(onMainThread: { // TODO: move to protocol function instead guard let controller = self?.delegate?.moleculeDelegate as? ViewController else { return } - controller.handleNewDataAndUpdateUI() + controller.handleNewData() }) } } diff --git a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift index 78bb1129..980f0dbf 100644 --- a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift +++ b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift @@ -45,11 +45,12 @@ public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior { for consumer in consumers { consumer.consume(notificationStatus: settings.authorizationStatus) } - // Tell template to update - MVMCoreDispatchUtility.performBlock(onMainThread: { + + Task { + // Tell template to update guard let controller = self.delegate?.moleculeDelegate as? ViewController else { return } - controller.handleNewDataAndUpdateUI() - }) + await controller.handleNewData() + } } } diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift new file mode 100644 index 00000000..7a92941f --- /dev/null +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -0,0 +1,104 @@ +// +// RefreshableMoleculeBehavior.swift +// MVMCoreUI +// +// Created by Kyle Hedden on 9/12/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore + +public class PollingBehaviorModel: PageBehaviorModelProtocol { + public class var identifier: String { "pollingBehavior" } + public var shouldAllowMultipleInstances: Bool { true } + + public let refreshInterval: TimeInterval + public let refreshAction: ActionModelProtocol + + public var runWhileHidden: Bool + public var refreshOnFirstLoad: Bool + public var refreshOnShown: Bool + + private enum CodingKeys: String, CodingKey { + case refreshInterval + case refreshAction + case runWhileHidden + case refreshOnFirstLoad + case refreshOnShown + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + refreshInterval = try typeContainer.decode(TimeInterval.self, forKey: .refreshInterval) + refreshAction = try typeContainer.decodeModel(codingKey: .refreshAction) + runWhileHidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .runWhileHidden) ?? false + refreshOnFirstLoad = try typeContainer.decodeIfPresent(Bool.self, forKey: .refreshOnFirstLoad) ?? false + refreshOnShown = try typeContainer.decodeIfPresent(Bool.self, forKey: .refreshOnShown) ?? false + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(refreshInterval, forKey: .refreshInterval) + try container.encode(refreshAction, forKey: .refreshAction) + try container.encode(runWhileHidden, forKey: .runWhileHidden) + try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad) + try container.encode(refreshOnShown, forKey: .refreshOnShown) + } +} + +public class PollingBehavior: NSObject, PageVisibilityBehavior { + + var model: PollingBehaviorModel + var delegateObject: MVMCoreUIDelegateObject? + var lastRefresh = Date.distantPast + var pollTimer: DispatchSourceTimer? + + var remainingTimeToRefresh: TimeInterval { + lastRefresh.timeIntervalSinceNow - model.refreshInterval + } + + var firstTimeLoad = true + + var refreshOnShown: Bool { + if model.refreshOnFirstLoad && firstTimeLoad { + return true + } + firstTimeLoad = false + return model.refreshOnShown + } + + public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { + self.model = model as! PollingBehaviorModel + self.delegateObject = delegateObject + } + + public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + resumePollingTimer(withRemainingTime: refreshOnShown ? 0 : remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval) + } + + public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) { + pollTimer?.cancel() + } + + func resumePollingTimer(withRemainingTime timeRemaining: TimeInterval, refreshAction: ActionModelProtocol, interval: TimeInterval) { + let delegateObject = delegateObject + pollTimer?.cancel() + pollTimer = DispatchSource.makeTimerSource() + pollTimer?.schedule(deadline: .now() + timeRemaining, repeating: interval) + pollTimer?.setEventHandler(qos:.utility) { + Task { + if let delegateActionHandler = delegateObject?.actionDelegate as? ActionDelegateProtocol { + try? await delegateActionHandler.performAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) + } else { + try? await MVMCoreActionHandler.shared()?.handleAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject) + } + } + } + pollTimer?.resume() + } + + deinit { + pollTimer?.cancel() + } +} diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerModelProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorContainerModelProtocol.swift similarity index 79% rename from MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerModelProtocol.swift rename to MVMCoreUI/Behaviors/Protocols/PageBehaviorContainerModelProtocol.swift index bb752694..1c2525f5 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerModelProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorContainerModelProtocol.swift @@ -1,16 +1,17 @@ // -// PageBehaviorHandlerModelProtocol.swift +// PageBehaviorConatinerModelProtocol.swift // MVMCoreUI // // Created by Scott Pfeil on 3/29/21. // Copyright © 2021 Verizon Wireless. All rights reserved. // -public protocol PageBehaviorHandlerModelProtocol { +/// Protocol applied to a model that contains a list of behavior models. +public protocol PageBehaviorContainerModelProtocol { var behaviors: [PageBehaviorModelProtocol]? { get set } } -public extension PageBehaviorHandlerModelProtocol { +public extension PageBehaviorContainerModelProtocol { /// Adds the behavior model to the behaviors if possible. mutating func add(behavior: PageBehaviorModelProtocol) { @@ -24,7 +25,7 @@ public extension PageBehaviorHandlerModelProtocol { } } -public extension PageBehaviorHandlerModelProtocol where Self: MoleculeTreeTraversalProtocol { +public extension PageBehaviorContainerModelProtocol where Self: MoleculeTreeTraversalProtocol { /// Traverses all models and adds any required behavior models. mutating func traverseAndAddRequiredBehaviors() { diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift index 325f8927..5c6a78bb 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift @@ -12,20 +12,10 @@ public protocol PageBehaviorHandlerProtocol { } public extension PageBehaviorHandlerProtocol { + /// Creates the behaviors and sets the variable. - mutating func createBehaviors(for model: PageBehaviorHandlerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { - - behaviors = behaviors?.filter { $0.transcendsPageUpdates } - if behaviors?.isEmpty ?? false { - behaviors = nil - } - - guard let behaviorModels = model.behaviors else { - return - } - - var behaviors: [PageBehaviorProtocol] = behaviors ?? [] - + func createBehaviors(for behaviorModels: [PageBehaviorModelProtocol], delegateObject: MVMCoreUIDelegateObject?) -> [PageBehaviorProtocol] { + var behaviors = [PageBehaviorProtocol]() for behaviorModel in behaviorModels { do { let handlerType = try ModelRegistry.getHandler(behaviorModel) as! PageBehaviorProtocol.Type @@ -37,7 +27,23 @@ public extension PageBehaviorHandlerProtocol { } } } + return behaviors + } + + mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { + // Pull the existing behaviors. + var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates } + // Create and append any new behaviors based on the incoming models. + let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject) + behaviors.append(contentsOf: newBehaviors) + + // Apply them to the page. self.behaviors = behaviors.count > 0 ? behaviors : nil + + // Ask the session to apply any more. (Curently inverted contol due to Swift <--> Obj-C conflict. + if let viewController = self as? UIViewController { + MVMCoreUISession.sharedGlobal()?.applyGlobalBehaviors(to: viewController) + } } /// Executes all behaviors of type. @@ -49,3 +55,11 @@ public extension PageBehaviorHandlerProtocol { return try behaviors?.compactMap({$0 as? T}).allSatisfy({ return try behaviourBlock($0) }) ?? true } } + +public extension PageBehaviorHandlerProtocol where Self: MVMCoreViewControllerProtocol { + + mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol) { + applyBehaviors(pageBehaviorModel: pageBehaviorModel, delegateObject: delegateObject?() as? MVMCoreUIDelegateObject) + } + +} diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index 543ee60c..ae09e0ce 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift @@ -14,12 +14,15 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol { /// Should the behavior persist regardless of page behavior model updates. var transcendsPageUpdates: Bool { get } + func modulesToListenFor() -> [String] + /// Initializes the behavior with the model init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) } public extension PageBehaviorProtocol { var transcendsPageUpdates: Bool { return false } + func modulesToListenFor() -> [String] { return [] } } /** @@ -32,6 +35,7 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) func didSetupNavigationBar(view: UINavigationBar, with model: NavigationItemModelProtocol) + func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject) throws -> Bool } @@ -42,11 +46,11 @@ public extension PageMoleculeTransformationBehavior { func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {} func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) {} func didSetupNavigationBar(view: UINavigationBar, with model: NavigationItemModelProtocol) {} + func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {} func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject) throws -> Bool { return true } } public protocol PageVisibilityBehavior: PageBehaviorProtocol { - func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) func willHidePage(_ delegateObject: MVMCoreUIDelegateObject?) @@ -84,8 +88,8 @@ public protocol PageCustomActionHandlerBehavior: PageBehaviorProtocol { } public extension MVMCoreUIDelegateObject { - var behaviorModelDelegate: PageBehaviorHandlerModelProtocol? { - (moleculeDelegate as? PageProtocol)?.pageModel as? PageBehaviorHandlerModelProtocol + var behaviorModelDelegate: PageBehaviorContainerModelProtocol? { + (moleculeDelegate as? PageProtocol)?.pageModel as? PageBehaviorContainerModelProtocol } weak var behaviorTemplateDelegate: (PageBehaviorHandlerProtocol & NSObjectProtocol)? { diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift new file mode 100644 index 00000000..2e38017c --- /dev/null +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -0,0 +1,110 @@ +// +// ReplacementMoleculeBehavior.swift +// MVMCoreUI +// +// Created by Kyle Hedden on 9/12/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore + +public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol { + public class var identifier: String { "replaceMoleculeBehavior" } + public var shouldAllowMultipleInstances: Bool { true } + public var moleculeIds: [String] +} + +public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior { + var moleculeIds: [String] + var modulesToListenFor: [String] + private var observingForResponses: NSObjectProtocol? + private var delegateObject: MVMCoreUIDelegateObject? + + public var transcendsPageUpdates: Bool { true } + + public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { + moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds + let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil + if shouldListenForListUpdates { + modulesToListenFor = [] + listenForModuleUpdates() + } else { + modulesToListenFor = moleculeIds + stopListeningForModuleUpdates() + } + self.delegateObject = delegateObject + guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return } + MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType) + } + + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) { + self.delegateObject = delegateObject + let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil + if shouldListenForListUpdates { + modulesToListenFor = [] + listenForModuleUpdates() + } else { + modulesToListenFor = moleculeIds + stopListeningForModuleUpdates() + } + + let moleculeModels = moleculeIds.compactMap { moleculeId in + do { + return try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId) + } catch { + let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! + if let error = error as? HumanReadableDecodingErrorProtocol { + coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)" + } + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + return nil + } + } + if moleculeModels.count > 0 { + delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels) + } + } + + private func listenForModuleUpdates() { + guard observingForResponses == nil else { return } + let pageUpdateQueue = OperationQueue() + pageUpdateQueue.maxConcurrentOperationCount = 1 + pageUpdateQueue.qualityOfService = .userInteractive + observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:)) + } + + private func stopListeningForModuleUpdates() { + guard let observingForResponses = observingForResponses else { return } + NotificationCenter.default.removeObserver(observingForResponses) + self.observingForResponses = nil + } + + @objc func responseJSONUpdated(notification: Notification) { + guard let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) else { return } + let modules: [MoleculeModelProtocol] = moleculeIds.compactMap { moleculeId in + guard let json = modulesLoaded.optionalDictionaryForKey(moleculeId) else { return nil } + do { + return try convertToModel(moduleJSON: json) + } catch { + let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))! + if let error = error as? HumanReadableDecodingErrorProtocol { + coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)" + } + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + return nil + } + } + if modules.count > 0 { + delegateObject?.moleculeDelegate?.replaceMoleculeData(modules) + } + } + + private func convertToModel(moduleJSON: [String: Any]) throws -> MoleculeModelProtocol { + guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName), + let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else { + throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName)) + } + return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as! MoleculeModelProtocol + } +} diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 2018c621..6a14767e 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -231,6 +231,8 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: PageGetContactBehavior.self, for: PageGetContactBehaviorModel.self) ModelRegistry.register(handler: AddRemoveMoleculesBehavior.self, for: AddRemoveMoleculesBehaviorModel.self) ModelRegistry.register(handler: GetNotificationAuthStatusBehavior.self, for: GetNotificationAuthStatusBehaviorModel.self) + ModelRegistry.register(handler: ReplaceableMoleculeBehavior.self, for: ReplaceableMoleculeBehaviorModel.self) + ModelRegistry.register(handler: PollingBehavior.self, for: PollingBehaviorModel.self) } open override class func registerActions() {