diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 5d09ab93..db03d4d9 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -572,9 +572,15 @@ DBEFFA04225A829700230692 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB891E822253FA8500022516 /* Label.swift */; }; EA05EFA9278DDE2C00828819 /* ClearFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA05EFA8278DDE2C00828819 /* ClearFormFieldEffectModel.swift */; }; EA05EFAB278DE53600828819 /* ClearableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA05EFAA278DE53600828819 /* ClearableModelProtocol.swift */; }; + EA1758482BC97ED800A5C0D9 /* BadgeIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758472BC97ED800A5C0D9 /* BadgeIndicator.swift */; }; + EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; }; + EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; }; + EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; }; EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; + EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642902BCDA97300D81DC4 /* TileContainer.swift */; }; + EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */; }; EA6E8B952B504A43000139B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B942B504A43000139B4 /* ButtonGroup.swift */; }; EA6E8B972B504A4D000139B4 /* ButtonGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */; }; EA7D81602B2B6E6800D29F9E /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D815F2B2B6E6800D29F9E /* Icon.swift */; }; @@ -1177,9 +1183,15 @@ DBC4391A224421A0001AB423 /* CaretLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaretLink.swift; sourceTree = ""; }; EA05EFA8278DDE2C00828819 /* ClearFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearFormFieldEffectModel.swift; sourceTree = ""; }; EA05EFAA278DE53600828819 /* ClearableModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearableModelProtocol.swift; sourceTree = ""; }; + EA1758472BC97ED800A5C0D9 /* BadgeIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicator.swift; sourceTree = ""; }; + EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = ""; }; + EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = ""; }; + EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = ""; }; EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = ""; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; + EA6642902BCDA97300D81DC4 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = ""; }; + EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainerModel.swift; sourceTree = ""; }; EA6E8B942B504A43000139B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = ""; }; EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupModel.swift; sourceTree = ""; }; EA7D815F2B2B6E6800D29F9E /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; @@ -2267,6 +2279,12 @@ AA07EA922510A451009A2AE3 /* Star.swift */, B4CC8FBE29DF34730005D28B /* BadgeModel.swift */, B4CC8FBC29DF34680005D28B /* Badge.swift */, + EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */, + EA1758472BC97ED800A5C0D9 /* BadgeIndicator.swift */, + EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */, + EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */, + EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */, + EA6642902BCDA97300D81DC4 /* TileContainer.swift */, EA985C3F2970939A00F2FF2E /* TileletModel.swift */, EA985C3D2970938F00F2FF2E /* Tilelet.swift */, EA7D81612B2B6E7F00D29F9E /* IconModel.swift */, @@ -2685,6 +2703,7 @@ D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */, AAE96FA225341F6A0037A989 /* ListStoreLocatorModel.swift in Sources */, D28A838923CCCFCB00DFE4FC /* LinkModel.swift in Sources */, + EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */, AA56A20F243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift in Sources */, AAB9C10824346F4B00151545 /* RadioSwatches.swift in Sources */, 94C2D9A923872E5E0006CF46 /* LabelAttributeImageModel.swift in Sources */, @@ -2909,6 +2928,7 @@ D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */, D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */, 0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */, + EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */, D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */, D260105D23D0BCD400764D80 /* Stack.swift in Sources */, 0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */, @@ -2936,6 +2956,7 @@ D22479962316AF6E003FCCF9 /* HeadlineBodyLink.swift in Sources */, 8DE5BECD2456F7A200772E76 /* ListTwoColumnDropdownSelectorsModel.swift in Sources */, AA7F47732541AD560015A2C1 /* ListStarRatingModel.swift in Sources */, + EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */, AA7F47762541AD6A0015A2C1 /* ListStarRating.swift in Sources */, 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */, AF7E509829E477C1009DC2AD /* AlertHandler.swift in Sources */, @@ -2990,6 +3011,7 @@ D264FAAC2441009400D98315 /* RadioBoxCollectionViewCell.swift in Sources */, BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */, D2D90B42240463E100DD6EC9 /* MoleculeHeaderModel.swift in Sources */, + EA1758482BC97ED800A5C0D9 /* BadgeIndicator.swift in Sources */, 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */, D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */, @@ -3054,6 +3076,7 @@ 0AE14F64238315D2005417F8 /* TextField.swift in Sources */, 0A51F3E22475CB73002E08B6 /* LoadingSpinnerModel.swift in Sources */, D2169303251E53D9002A6324 /* SectionListTemplateModel.swift in Sources */, + EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */, AF7E509929E477C1009DC2AD /* AlertController.swift in Sources */, 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */, BB105859248DEFF70069D008 /* UICollectionViewLeftAlignedLayout.swift in Sources */, @@ -3181,6 +3204,7 @@ D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */, 8D4687E2242E2DE400802879 /* ListFourColumnDataUsageListItemModel.swift in Sources */, D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */, + EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */, 012A88C2238D7BCA00FE3DA1 /* CarouselItemModel.swift in Sources */, 27F6B08C26052AFF008529AA /* ParentMoleculeModelProtocol.swift in Sources */, EA985C402970939A00F2FF2E /* TileletModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicator.swift b/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicator.swift new file mode 100644 index 00000000..3eb78919 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicator.swift @@ -0,0 +1,70 @@ +// +// BadgeIndicator.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/12/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class BadgeIndicator: VDS.BadgeIndicator, VDSMoleculeViewProtocol { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var viewModel: BadgeIndicatorModel! + + public var delegateObject: MVMCoreUIDelegateObject? + + public var additionalData: [AnyHashable : Any]? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + + public func viewModelDidUpdate() { + surface = viewModel.surface + number = viewModel.number + fillColor = viewModel.fillColor + borderColorLight = viewModel.borderColorLight?.uiColor + borderColorDark = viewModel.borderColorDark?.uiColor + kind = viewModel.kind + maximumDigits = viewModel.maximumDigits + size = viewModel.size + leadingCharacter = viewModel.leadingCharacter + trailingText = viewModel.trailingText + dotSize = viewModel.dotSize + verticalPadding = viewModel.verticalPadding + horizontalPadding = viewModel.horizontalPadding + hideDot = viewModel.hideDot + hideBorder = viewModel.hideBorder + width = viewModel.width + height = viewModel.height + } + + public func updateView(_ size: CGFloat) {} + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func updateAccessibility() { + super.updateAccessibility() + + if let viewModel { + if let accessibilityText = viewModel.accessibilityText { + self.accessibilityLabel = accessibilityText + } + } + } + +} + +//to deal with how it's parent constrains this control +extension BadgeIndicator: MVMCoreUIViewConstrainingProtocol { + + public func needsToBeConstrained() -> Bool { true } + + public func horizontalAlignment() -> UIStackView.Alignment { .leading } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicatorModel.swift new file mode 100644 index 00000000..bdc724b6 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/BadgeIndicatorModel.swift @@ -0,0 +1,111 @@ +// +// BadgeIndicatorModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/12/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + + +open class BadgeIndicatorModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String { "badgeIndicator" } + public var id: String = UUID().uuidString + public var backgroundColor: Color? + + //-------------------------------------------------- + // MARK: - VDS Properties + //-------------------------------------------------- + public var surface: Surface { inverted ? .dark : .light } + public var inverted: Bool = false + public var number: Int? + public var accessibilityText: String? + public var fillColor = BadgeIndicator.FillColor.red + public var borderColorLight: Color? + public var borderColorDark: Color? + public var kind = BadgeIndicator.Kind.simple + public var maximumDigits = BadgeIndicator.MaximumDigits.two + public var size = BadgeIndicator.Size.xxlarge + public var leadingCharacter: String? + public var trailingText: String? + public var dotSize: CGFloat? + public var verticalPadding: CGFloat? + public var horizontalPadding: CGFloat? + public var hideDot: Bool = false + public var hideBorder: Bool = false + public var width: CGFloat? + public var height: CGFloat? + + private enum CodingKeys: String, CodingKey { + case id + case inverted + case accessibilityText + case number + case fillColor + case borderColorLight + case borderColorDark + case kind + case maximumDigits + case size + case leadingCharacter + case trailingText + case dotSize + case verticalPadding + case horizontalPadding + case hideDot + case hideBorder + case width + case height + } + + required public convenience init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.init() + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false + accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText) + number = try container.decodeIfPresent(Int.self, forKey: .number) + fillColor = try container.decodeIfPresent(BadgeIndicator.FillColor.self, forKey: .fillColor) ?? .red + borderColorLight = try container.decodeIfPresent(Color.self, forKey: .borderColorLight) + borderColorDark = try container.decodeIfPresent(Color.self, forKey: .borderColorDark) + kind = try container.decodeIfPresent(BadgeIndicator.Kind.self, forKey: .kind) ?? .simple + maximumDigits = try container.decodeIfPresent(BadgeIndicator.MaximumDigits.self, forKey: .maximumDigits) ?? .two + size = try container.decodeIfPresent(BadgeIndicator.Size.self, forKey: .size) ?? .xxlarge + leadingCharacter = try container.decodeIfPresent(String.self, forKey: .leadingCharacter) + trailingText = try container.decodeIfPresent(String.self, forKey: .trailingText) + dotSize = try container.decodeIfPresent(CGFloat.self, forKey: .dotSize) + verticalPadding = try container.decodeIfPresent(CGFloat.self, forKey: .verticalPadding) + horizontalPadding = try container.decodeIfPresent(CGFloat.self, forKey: .horizontalPadding) + hideDot = try container.decodeIfPresent(Bool.self, forKey: .hideDot) ?? false + hideBorder = try container.decodeIfPresent(Bool.self, forKey: .hideBorder) ?? false + width = try container.decodeIfPresent(CGFloat.self, forKey: .width) + height = try container.decodeIfPresent(CGFloat.self, forKey: .height) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(inverted, forKey: .inverted) + try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) + try container.encodeIfPresent(number, forKey: .number) + try container.encodeIfPresent(fillColor, forKey: .fillColor) + try container.encodeIfPresent(borderColorLight, forKey: .borderColorLight) + try container.encodeIfPresent(borderColorDark, forKey: .borderColorDark) + try container.encodeIfPresent(kind, forKey: .kind) + try container.encodeIfPresent(maximumDigits, forKey: .maximumDigits) + try container.encodeIfPresent(size, forKey: .size) + try container.encodeIfPresent(leadingCharacter, forKey: .leadingCharacter) + try container.encodeIfPresent(trailingText, forKey: .trailingText) + try container.encodeIfPresent(dotSize, forKey: .dotSize) + try container.encodeIfPresent(verticalPadding, forKey: .verticalPadding) + try container.encodeIfPresent(hideDot, forKey: .hideDot) + try container.encodeIfPresent(hideBorder, forKey: .hideBorder) + try container.encodeIfPresent(width, forKey: .width) + try container.encodeIfPresent(height, forKey: .height) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/ButtonIcon.swift b/MVMCoreUI/Atomic/Atoms/Views/ButtonIcon.swift new file mode 100644 index 00000000..389033cb --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/ButtonIcon.swift @@ -0,0 +1,72 @@ +// +// ButtonIcon.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/12/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class ButtonIcon: VDS.ButtonIcon, VDSMoleculeViewProtocol { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var viewModel: ButtonIconModel! + + public var delegateObject: MVMCoreUIDelegateObject? + + public var additionalData: [AnyHashable : Any]? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + + public func viewModelDidUpdate() { + surface = viewModel.surface + + badgeIndicatorModel = viewModel.badgeIndicatorModel + kind = viewModel.kind + surfaceType = viewModel.surfaceType + iconName = viewModel.iconName + selectedIconName = viewModel.selectedIconName + size = viewModel.size + customSize = viewModel.customSize + floating = viewModel.floating + fitToIcon = viewModel.fitToIcon + hideBorder = viewModel.hideBorder + showBadgeIndicator = viewModel.showBadgeIndicator + selectable = viewModel.selectable + iconOffset = viewModel.iconOffset + + } + + public func updateView(_ size: CGFloat) {} + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func updateAccessibility() { + super.updateAccessibility() + + if let viewModel { + if let accessibilityText = viewModel.accessibilityText { + //since this is a container control and the + //icon & badgeIndicator (gets from it's own model) are traversed separatly + icon.accessibilityLabel = accessibilityText + } + } + } + +} + +//to deal with how it's parent constrains this control +extension ButtonIcon: MVMCoreUIViewConstrainingProtocol { + + public func needsToBeConstrained() -> Bool { true } + + public func horizontalAlignment() -> UIStackView.Alignment { .leading } +} + diff --git a/MVMCoreUI/Atomic/Atoms/Views/ButtonIconModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ButtonIconModel.swift new file mode 100644 index 00000000..ed93e969 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/ButtonIconModel.swift @@ -0,0 +1,142 @@ +// +// ButtonIconModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/12/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class ButtonIconModel: ButtonModelProtocol, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "buttonIcon" + public var id: String = UUID().uuidString + public var backgroundColor: Color? + + //-------------------------------------------------- + // MARK: - VDS Properties + //-------------------------------------------------- + public var surface: Surface { inverted ? .dark : .light } + public var inverted: Bool = false + public var accessibilityText: String? + + public var action: ActionModelProtocol + + public var kind = ButtonIcon.Kind.ghost + public var surfaceType = ButtonIcon.SurfaceType.colorFill + public var iconName: Icon.Name = .info + public var selectedIconName: Icon.Name? + public var size = ButtonIcon.Size.large + public var customSize : Int? + public var floating: Bool = false + public var fitToIcon: Bool = false + public var hideBorder: Bool = true + public var showBadgeIndicator: Bool = false + public var selectable: Bool = false + public var iconOffset: CGPoint = .zero + + public var badgeIndicatorModel: VDS.ButtonIcon.BadgeIndicatorModel? { + guard let model = badgeIndicator else { return nil } + return .init(kind: model.kind, + fillColor: model.fillColor, + expandDirection: expandDirection, + size: model.size, + maximumDigits: model.maximumDigits, + width: model.width, + height: model.height, + number: model.number, + leadingCharacter: model.leadingCharacter, + trailingText: model.trailingText, + dotSize: model.dotSize, + verticalPadding: model.verticalPadding, + horizontalPadding: model.horizontalPadding, + hideDot: model.hideDot, + hideBorder: model.hideBorder) + } + + private var badgeIndicator: BadgeIndicatorModel? + private var expandDirection = ButtonIcon.BadgeIndicatorModel.ExpandDirection.right + + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public init(with iconName: VDS.Icon.Name, action: ActionModelProtocol) { + self.iconName = iconName + self.action = action + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { + case id + case inverted + case accessibilityText + case action + case badgeIndicator + case expandDirection + case kind + case surfaceType + case iconName + case selectedIconName + case size + case customSize + case floating + case fitToIcon + case hideBorder + case showBadgeIndicator + case selectable + case iconOffset + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + action = try container.decodeModel(codingKey: .action) + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false + accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText) + badgeIndicator = try container.decodeIfPresent(BadgeIndicatorModel.self, forKey: .badgeIndicator) + expandDirection = try container.decodeIfPresent(ButtonIcon.BadgeIndicatorModel.ExpandDirection.self, forKey: .expandDirection) ?? .right + kind = try container.decodeIfPresent(ButtonIcon.Kind.self, forKey: .kind) ?? .ghost + surfaceType = try container.decodeIfPresent(ButtonIcon.SurfaceType.self, forKey: .kind) ?? .colorFill + iconName = try container.decode(Icon.Name.self, forKey: .iconName) + selectedIconName = try container.decodeIfPresent(Icon.Name.self, forKey: .selectedIconName) + size = try container.decodeIfPresent(ButtonIcon.Size.self, forKey: .size) ?? .large + customSize = try container.decodeIfPresent(Int.self, forKey: .customSize) + floating = try container.decodeIfPresent(Bool.self, forKey: .floating) ?? false + fitToIcon = try container.decodeIfPresent(Bool.self, forKey: .fitToIcon) ?? false + hideBorder = try container.decodeIfPresent(Bool.self, forKey: .hideBorder) ?? false + showBadgeIndicator = try container.decodeIfPresent(Bool.self, forKey: .showBadgeIndicator) ?? false + selectable = try container.decodeIfPresent(Bool.self, forKey: .selectable) ?? false + iconOffset = try container.decodeIfPresent(CGPoint.self, forKey: .iconOffset) ?? .zero + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(inverted, forKey: .inverted) + try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) + try container.encodeIfPresent(badgeIndicator, forKey: .badgeIndicator) + try container.encodeIfPresent(expandDirection, forKey: .expandDirection) + try container.encodeIfPresent(kind, forKey: .kind) + try container.encodeIfPresent(surfaceType, forKey: .kind) + try container.encode(iconName, forKey: .iconName) + try container.encodeIfPresent(selectedIconName, forKey: .selectedIconName) + try container.encodeIfPresent(size, forKey: .size) + try container.encodeIfPresent(customSize, forKey: .customSize) + try container.encodeIfPresent(floating, forKey: .floating) + try container.encodeIfPresent(fitToIcon, forKey: .fitToIcon) + try container.encodeIfPresent(hideBorder, forKey: .hideBorder) + try container.encodeIfPresent(showBadgeIndicator, forKey: .showBadgeIndicator) + try container.encodeIfPresent(selectable, forKey: .selectable) + try container.encodeIfPresent(iconOffset, forKey: .iconOffset) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift new file mode 100644 index 00000000..57b8b8f2 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainer.swift @@ -0,0 +1,134 @@ +// +// TileContainer.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/15/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS +import Combine + +open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{ + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public var model: MoleculeModelProtocol? + + public var viewModel: TileContainerModel! + public var delegateObject: MVMCoreUIDelegateObject? + public var additionalData: [AnyHashable: Any]? + + public var molecule: MoleculeViewProtocol? { + willSet { + if newValue == nil { + molecule?.removeFromSuperview() + } + } + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public convenience required init() { + self.init(frame: .zero) + } + + //-------------------------------------------------- + // MARK: - Public + //-------------------------------------------------- + public func viewModelDidUpdate() { + + if let moleculeModel = viewModel.molecule { + if let molecule, + moleculeModel.moleculeName == molecule.model?.moleculeName { + molecule.set(with: moleculeModel, delegateObject, additionalData) + } else if let moleculeView = ModelRegistry.createMolecule(moleculeModel, delegateObject: delegateObject, additionalData: additionalData) { + molecule = moleculeView + addContentView(moleculeView) + } + } + + if let imageName = viewModel.backgroundImage { + loadImage(imageName) + } + + padding = viewModel.padding + color = viewModel.color + backgroundEffect = viewModel.backgroundEffect + aspectRatio = viewModel.aspectRatio + width = viewModel.width + height = viewModel.height + showBorder = viewModel.showBorder + showDropShadows = viewModel.showDropShadwows + //setup action + if let action = viewModel.action { + //add the subscriber + onClick = { [weak self] control in + guard let self, let viewModel = self.viewModel else { return } + MVMCoreUIActionHandler.performActionUnstructured(with: action, + sourceModel: viewModel, + additionalData: self.additionalData, + delegateObject: self.delegateObject) + } + } + } + + private func loadImage(_ imageName: String? = nil) { + guard let imageName else { + if backgroundImage != nil { + backgroundImage = nil + } + return + } + + let finishedLoadingBlock: MVMCoreGetImageBlock = {[weak self] (image, data, isFallbackImage) in MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + self.backgroundImage = image + })} + MVMCoreCache.shared()?.getImage(imageName, useWidth: false, widthForS7: 0, useHeight: false, heightForS7: 0, format: nil, localFallbackImageName: nil, allowServerQueryParameters: false, localBundle: nil, completionHandler: finishedLoadingBlock) + } + + //-------------------------------------------------- + // MARK: - MVMCoreViewProtocol + //-------------------------------------------------- + open func updateView(_ size: CGFloat) {} + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + //since this is a class func, we can't reference it directly + public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + 100 + } + + /// Allows the molecule to set its name for reuse. Default could be moleculeName. Mainly used for list or collections. + public static func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { + // This will aggregate names of molecules to make an id. + guard let containerModel = model as? TileContainerModel, + let molecule = containerModel.molecule, + let moleculeClass = ModelRegistry.getMoleculeClass(molecule), + let moleculeName = moleculeClass.nameForReuse(with: molecule, delegateObject) + else { return "\(model.moleculeName)<>" } + + return "\(model.moleculeName)<\(moleculeName)>" + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any collection size changes + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self) + } + } +} + +extension TileContainer: MVMCoreUIViewConstrainingProtocol { + public func horizontalAlignment() -> UIStackView.Alignment { .leading } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift new file mode 100644 index 00000000..b6932ef9 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/TileContainerModel.swift @@ -0,0 +1,107 @@ +// +// TileContainerModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 4/15/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class TileContainerModel: TileContainerBaseModel, ParentMoleculeModelProtocol, MoleculeModelProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "tileContainer" + public var id: String = UUID().uuidString + public var backgroundColor: Color? + + public var molecule: MoleculeModelProtocol? + public var children: [any MoleculeModelProtocol] { + guard let molecule else { return [] } + return [molecule] + } + + public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { + return try replaceChildMolecule(at: &self.molecule, with: molecule) + } + + private enum CodingKeys: String, CodingKey { + case id + case moleculeName + case molecule + } + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + molecule = try container.decodeModelIfPresent(codingKey: .molecule) + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeModelIfPresent(molecule, forKey: .molecule) + try super.encode(to: encoder) + } +} + + +open class TileContainerBaseModel> : Codable{ + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var backgroundImage: String? + public var action: ActionModelProtocol? + public var imageFallbackColor: Surface = .light + public var width: CGFloat? + public var height: CGFloat? + public var showBorder: Bool = false + public var showDropShadwows: Bool = false + public var padding = PaddingType.defaultValue + public var color: TileContainerType.BackgroundColor = .black + public var aspectRatio: TileContainerType.AspectRatio = .ratio1x1 + public var backgroundEffect: TileContainerType.BackgroundEffect = .none + + private enum CodingKeys: String, CodingKey { + case backgroundImage + case action + case padding + case color + case aspectRatio + case imageFallbackColor + case width + case height + case showBorder + case showDropShadows + case backgroundEffect + } + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + backgroundImage = try container.decodeIfPresent(String.self, forKey: .backgroundImage) + width = try container.decodeIfPresent(CGFloat.self, forKey: .width) + height = try container.decodeIfPresent(CGFloat.self, forKey: .height) + action = try container.decodeModelIfPresent(codingKey: .action) + padding = try container.decodeIfPresent(PaddingType.self, forKey: .padding) ?? PaddingType.defaultValue + color = try container.decodeIfPresent(TileContainerType.BackgroundColor.self, forKey: .color) ?? .black + aspectRatio = try container.decodeIfPresent(TileContainerType.AspectRatio.self, forKey: .aspectRatio) ?? .ratio1x1 + backgroundEffect = try container.decodeIfPresent(TileContainerType.BackgroundEffect.self, forKey: .backgroundEffect) ?? .none + + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(backgroundImage, forKey: .backgroundImage) + try container.encodeIfPresent(width, forKey: .width) + try container.encodeIfPresent(height, forKey: .height) + try container.encodeIfPresent(backgroundEffect, forKey: .backgroundEffect) + try container.encodeIfPresent(padding, forKey: .padding) + try container.encodeIfPresent(color, forKey: .color) + try container.encodeIfPresent(aspectRatio, forKey: .aspectRatio) + try container.encodeModelIfPresent(action, forKey: .action) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift index 09f905e7..28ece685 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift @@ -39,10 +39,16 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{ // MARK: - Public //-------------------------------------------------- public func viewModelDidUpdate() { - color = viewModel.color + padding = viewModel.padding + color = viewModel.color + backgroundEffect = viewModel.backgroundEffect aspectRatio = viewModel.aspectRatio width = viewModel.width + height = viewModel.height + showBorder = viewModel.showBorder + showDropShadows = viewModel.showDropShadwows + if let value = viewModel.textWidth { textWidth = .value(value) } else if let percentage = viewModel.textPercentage { diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index 62fac7e9..b287a9ff 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -9,7 +9,7 @@ import Foundation import VDS -open class TileletModel: MoleculeModelProtocol { +open class TileletModel: TileContainerBaseModel, MoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties @@ -17,52 +17,37 @@ open class TileletModel: MoleculeModelProtocol { public static var identifier: String = "tilelet" public var id: String = UUID().uuidString public var backgroundColor: Color? - public var color: Tilelet.BackgroundColor - public var padding: Tilelet.Padding - public var aspectRatio: Tilelet.AspectRatio + public var badge: Tilelet.BadgeModel? public var title: LabelModel? public var subTitle: LabelModel? public var descriptiveIcon: Tilelet.DescriptiveIcon? public var directionalIcon: Tilelet.DirectionalIcon? - public var width: CGFloat? public var textWidth: CGFloat? public var textPercentage: CGFloat? - public var action: ActionModelProtocol? private enum CodingKeys: String, CodingKey { case id case moleculeName - case backgroundColor - case color - case padding - case aspectRatio case badge case title case subTitle case descriptiveIcon case directionalIcon - case width case textWidth case textPercentage - case action } required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString - self.backgroundColor = try container.decodeIfPresent(Color.self, forKey: .backgroundColor) - self.color = try container.decodeIfPresent(Tilelet.BackgroundColor.self, forKey: .color) ?? Tilelet.BackgroundColor.black - self.padding = try container.decodeIfPresent(Tilelet.Padding.self, forKey: .padding) ?? Tilelet.Padding.small - self.aspectRatio = try container.decodeIfPresent(Tilelet.AspectRatio.self, forKey: .aspectRatio) ?? Tilelet.AspectRatio.none - self.badge = try container.decodeIfPresent(Tilelet.BadgeModel.self, forKey: .badge) - self.title = try container.decodeIfPresent(LabelModel.self, forKey: .title) - self.subTitle = try container.decodeIfPresent(LabelModel.self, forKey: .subTitle) - self.descriptiveIcon = try container.decodeIfPresent(Tilelet.DescriptiveIcon.self, forKey: .descriptiveIcon) - self.directionalIcon = try container.decodeIfPresent(Tilelet.DirectionalIcon.self, forKey: .directionalIcon) - self.width = try container.decodeIfPresent(CGFloat.self, forKey: .width) - self.textWidth = try container.decodeIfPresent(CGFloat.self, forKey: .textWidth) - self.textPercentage = try container.decodeIfPresent(CGFloat.self, forKey: .textPercentage) - action = try container.decodeModelIfPresent(codingKey: .action) + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + badge = try container.decodeIfPresent(Tilelet.BadgeModel.self, forKey: .badge) + title = try container.decodeIfPresent(LabelModel.self, forKey: .title) + subTitle = try container.decodeIfPresent(LabelModel.self, forKey: .subTitle) + descriptiveIcon = try container.decodeIfPresent(Tilelet.DescriptiveIcon.self, forKey: .descriptiveIcon) + directionalIcon = try container.decodeIfPresent(Tilelet.DirectionalIcon.self, forKey: .directionalIcon) + textWidth = try container.decodeIfPresent(CGFloat.self, forKey: .textWidth) + textPercentage = try container.decodeIfPresent(CGFloat.self, forKey: .textPercentage) + try super.init(from: decoder) } public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? { @@ -99,22 +84,17 @@ open class TileletModel: MoleculeModelProtocol { return .init(text: subTitle.text, textAttributes: attrs) } - public func encode(to encoder: Encoder) throws { + public override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) - try container.encodeIfPresent(color, forKey: .color) - try container.encodeIfPresent(padding, forKey: .padding) - try container.encodeIfPresent(aspectRatio, forKey: .aspectRatio) try container.encodeIfPresent(badge, forKey: .badge) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(subTitle, forKey: .subTitle) try container.encodeIfPresent(descriptiveIcon, forKey: .descriptiveIcon) try container.encodeIfPresent(directionalIcon, forKey: .directionalIcon) - try container.encodeIfPresent(width, forKey: .width) try container.encodeIfPresent(textWidth, forKey: .textWidth) try container.encodeIfPresent(textPercentage, forKey: .textPercentage) - try container.encodeModelIfPresent(action, forKey: .action) + try super.encode(to: encoder) } } diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index d78e7be6..f514248e 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -16,6 +16,14 @@ import VDSTokens extension VDS.Surface: Codable {} extension VDS.Badge.FillColor: Codable {} +extension VDS.BadgeIndicator.FillColor: Codable {} +extension VDS.BadgeIndicator.Kind: Codable {} +extension VDS.BadgeIndicator.MaximumDigits: Codable {} +extension VDS.BadgeIndicator.Size: Codable {} +extension VDS.ButtonIcon.Kind: Codable {} +extension VDS.ButtonIcon.Size: Codable {} +extension VDS.ButtonIcon.BadgeIndicatorModel.ExpandDirection: Codable {} +extension VDS.ButtonIcon.SurfaceType: Codable {} extension VDS.ButtonGroup.Alignment: Codable {} extension VDS.Icon.Name: Codable {} extension VDS.Icon.Size: Codable {} @@ -92,6 +100,53 @@ extension VDS.TileContainerBase.BackgroundColor: Codable { } } +extension VDS.TileContainerBase.BackgroundEffect: Codable { + + enum CodingKeys: String, CodingKey { + case type + case firstColor + case secondColor + } + + enum BackgroundEffectType: String, Codable { + case transparency + case none + case gradient + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(BackgroundEffectType.self, forKey: .type) + + switch type { + case .transparency: + self = .transparency + case .none: + self = .none + case .gradient: + let firstColor = try container.decode(String.self, forKey: .firstColor) + let secondColor = try container.decode(String.self, forKey: .secondColor) + self = .gradient(firstColor, secondColor) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .transparency: + try container.encode(BackgroundEffectType.transparency.rawValue, forKey: .type) + case .none: + try container.encode(BackgroundEffectType.none.rawValue, forKey: .type) + case .gradient(let firstColor, let secondColor): + try container.encode(BackgroundEffectType.gradient.rawValue, forKey: .type) + try container.encode(firstColor, forKey: .firstColor) + try container.encode(secondColor, forKey: .secondColor) + @unknown default: + break + } + } +} + extension VDS.TileContainer.Padding: Codable { enum PaddingError: Error { case valueNotFound(type: String) diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 6a14767e..bbde1bf9 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -73,8 +73,11 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: LoadingSpinner.self, for: LoadingSpinnerModel.self) ModelRegistry.register(handler: Video.self, for: VideoModel.self) ModelRegistry.register(handler: Tilelet.self, for: TileletModel.self) + ModelRegistry.register(handler: TileContainer.self, for: TileContainerModel.self) ModelRegistry.register(handler: Badge.self, for: BadgeModel.self) + ModelRegistry.register(handler: BadgeIndicator.self, for: BadgeIndicatorModel.self) ModelRegistry.register(handler: Icon.self, for: IconModel.self) + ModelRegistry.register(handler: ButtonIcon.self, for: ButtonIconModel.self) ModelRegistry.register(handler: Tooltip.self, for: TooltipModel.self) // MARK:- Horizontal Combination Molecules