diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 9d787e35..8619ce46 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -298,6 +298,8 @@ AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */; }; AFE4A1D127DFB5EE00C458D0 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */; }; AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */; }; + B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBE29DF34730005D28B /* BadgeModel.swift */; }; + B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBC29DF34680005D28B /* Badge.swift */; }; BB105859248DEFF70069D008 /* UICollectionViewLeftAlignedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */; }; BB1D17E0244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */; }; BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */; }; @@ -574,8 +576,8 @@ EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; }; EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */; }; - EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilelet.swift */; }; EA985C402970939A00F2FF2E /* TileletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3F2970939A00F2FF2E /* TileletModel.swift */; }; + EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilelet.swift */; }; EA985C602970A3F000F2FF2E /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C5F2970A3F000F2FF2E /* VDS.framework */; }; EA985C642970A40E00F2FF2E /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C632970A40E00F2FF2E /* VDSTypographyTokens.xcframework */; }; EA985C852981AA9C00F2FF2E /* VDS-Enums+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C842981AA9C00F2FF2E /* VDS-Enums+Codable.swift */; }; @@ -888,6 +890,8 @@ AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = ""; }; AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = ""; }; AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; + B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = ""; }; + B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewLeftAlignedLayout.swift; sourceTree = ""; }; BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMediumModel.swift; sourceTree = ""; }; BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMedium.swift; sourceTree = ""; }; @@ -2234,8 +2238,10 @@ AA37CBD42519072F0027344C /* Stars.swift */, AA07EA902510A442009A2AE3 /* StarModel.swift */, AA07EA922510A451009A2AE3 /* Star.swift */, - EA985C3D2970938F00F2FF2E /* Tilelet.swift */, + B4CC8FBE29DF34730005D28B /* BadgeModel.swift */, + B4CC8FBC29DF34680005D28B /* Badge.swift */, EA985C3F2970939A00F2FF2E /* TileletModel.swift */, + EA985C3D2970938F00F2FF2E /* Tilelet.swift */, ); path = Views; sourceTree = ""; @@ -2938,6 +2944,7 @@ 27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */, D2A5146B2214905000345BFB /* ThreeLayerViewController.swift in Sources */, 526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */, + B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */, 8D24041523E7FC0B009E23BE /* ListLeftVariableIconWithRightCaretModel.swift in Sources */, 3265B30224BCA737000D154B /* HeadersH1NoButtonsBodyTextModel.swift in Sources */, D28A838F23CCDEDE00DFE4FC /* TwoButtonViewModel.swift in Sources */, @@ -3047,6 +3054,7 @@ 22B678F929E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift in Sources */, BB2FB3BB247E7EBC00DF73CD /* TagCollectionViewCell.swift in Sources */, 012A88C6238DA34000FE3DA1 /* ModuleMoleculeModel.swift in Sources */, + B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */, 94C2D9A123872BCC0006CF46 /* LabelAttributeUnderlineModel.swift in Sources */, EA985C852981AA9C00F2FF2E /* VDS-Enums+Codable.swift in Sources */, AAB8549824DC01BD00477C40 /* ListThreeColumnBillHistoryDividerModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/Badge.swift b/MVMCoreUI/Atomic/Atoms/Views/Badge.swift new file mode 100644 index 00000000..5e8c2e25 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Badge.swift @@ -0,0 +1,60 @@ +// +// Badge.swift +// MVMCoreUI +// +// Created by Rebecca Antonelli on 4/6/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore +import VDS +import Combine + +open class Badge: VDS.Badge, VDSMoleculeViewProtocol { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var viewModel: BadgeModel! + + public var delegateObject: MVMCoreUIDelegateObject? + + public var additionalData: [AnyHashable : Any]? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + + public func viewModelDidUpdate() { + text = viewModel.text + maxWidth = viewModel.maxWidth + numberOfLines = viewModel.numberOfLines + fillColor = viewModel.fillColor + surface = viewModel.surface + } + + 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 Badge: MVMCoreUIViewConstrainingProtocol { + + public func needsToBeConstrained() -> Bool { true } + + public func horizontalAlignment() -> UIStackView.Alignment { .leading } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift new file mode 100644 index 00000000..b739baf7 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift @@ -0,0 +1,56 @@ +// +// BadgeModel.swift +// MVMCoreUI +// +// Created by Rebecca Antonelli on 4/6/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class BadgeModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "badge" + public var id: String = UUID().uuidString + public var backgroundColor: Color? + + //-------------------------------------------------- + // MARK: - VDS Properties + //-------------------------------------------------- + public var text: String = "" + public var accessibilityText: String? + public var maxWidth: CGFloat? + public var numberOfLines: Int = 1 + public var fillColor = Badge.FillColor.red + public var surface: Surface = .light + + private enum CodingKeys: String, CodingKey { + case id, text, accessibilityText, fillColor, surface, numberOfLines, maxWidth + } + + required public convenience init(from decoder: Decoder) throws { + self.init() + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + text = try container.decode(String.self, forKey: .text) + accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText) + fillColor = try container.decodeIfPresent(Badge.FillColor.self, forKey: .fillColor) ?? .red + surface = try container.decodeIfPresent(Surface.self, forKey: .surface) ?? .light + numberOfLines = try container.decodeIfPresent(Int.self, forKey: .numberOfLines) ?? 1 + maxWidth = try container.decodeIfPresent(CGFloat.self, forKey: .maxWidth) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(text, forKey: .text) + try container.encode(accessibilityText, forKey: .accessibilityText) + try container.encode(fillColor, forKey: .fillColor) + try container.encode(surface, forKey: .surface) + try container.encode(numberOfLines, forKey: .numberOfLines) + try container.encodeIfPresent(maxWidth, forKey: .maxWidth) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocator.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocator.swift index 1e271e87..5ff57919 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocator.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocator.swift @@ -11,149 +11,77 @@ // MARK: - Outlets //-------------------------------------------------- - public let heart = Heart() - public let leftHeadline = Label(fontStyle: .BoldBodySmall) - public let leftBody = Label(fontStyle: .RegularBodySmall) + public let badge = Badge() + public let leftHeadline = Label(fontStyle: .BoldTitleSmall) + public let leftBody = Label(fontStyle: .RegularMicro) public let leftSubBody = Label(fontStyle: .RegularBodySmall) public let rightLabel = Label(fontStyle: .RegularBodySmall) - private lazy var rightLabelStackItem: StackItem = { - StackItem(andContain: rightLabel) - }() + public var model: ListStoreLocatorModel? public lazy var horizontalStack: Stack = { return Stack(with: StackModel(molecules: [StackItemModel(horizontalAlignment: .fill), - StackItemModel(horizontalAlignment: .fill), - StackItemModel(horizontalAlignment: .trailing)], - axis: .horizontal, spacing: Padding.Two), stackItems: [StackItem(andContain: leftHeadline), StackItem(andContain: heart), rightLabelStackItem]) + StackItemModel(horizontalAlignment: .trailing, verticalAlignment: .center)], + axis: .horizontal, spacing: Padding.Two), stackItems: [StackItem(andContain: stack), StackItem(andContain: rightLabel)]) }() + public lazy var stack: Stack = { - return Stack.createStack(with: [horizontalStack, leftBody, leftSubBody], axis: .vertical, spacing: 0) - }() - public var sizeObject: MFSizeObject? = MFSizeObject(standardSize: 12, standardiPadPortraitSize: 18) + return Stack(with: .init(molecules: [StackItemModel(horizontalAlignment: .leading), + StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill)], + axis: .vertical, spacing: Padding.One), stackItems: [StackItem(andContain: badge), StackItem(andContain: leftHeadline), StackItem(andContain: leftBody), StackItem(andContain: leftSubBody)]) + + }() //------------------------------------------------------- // MARK: - Lifecycle //------------------------------------------------------- open override func setupView() { super.setupView() - + rightLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 900), for: .horizontal) rightLabel.setContentHuggingPriority(UILayoutPriority(rawValue: 900), for: .horizontal) - addMolecule(stack) + addMolecule(horizontalStack) stack.restack() horizontalStack.restack() + leftSubBody.textColor = UIColor.mfTextLightGray() } - public override func updateView(_ size: CGFloat) { - super.updateView(size) - if let dimension = sizeObject?.getValueBased(onSize: size) { - heart.widthConstraint?.constant = dimension - } - } - //------------------------------------------------------ // MARK: - Molecule //------------------------------------------------------ open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) - guard let model = model as? ListStoreLocatorModel else { return } - horizontalStack.updateContainedMolecules(with: [model.leftHeadline, model.heart, model.rightLabel], delegateObject, additionalData) - leftBody.set(with: model.leftBody, delegateObject, additionalData) - leftSubBody.set(with: model.leftSubBody, delegateObject, additionalData) + stack.updateContainedMolecules(with: [model.badge, model.leftHeadline, model.leftBody, model.leftSubBody], delegateObject, additionalData) + rightLabel.set(with: model.rightLabel, delegateObject, additionalData) + self.model = model updateAccessibilityLabel() } - open override func alignAccessoryToHero() -> CGPoint? { - let heroCenter = super.alignAccessoryToHero() - - if let heroCenter = heroCenter { - let convertedPoint = horizontalStack.convert(heroCenter, from: self) - rightLabelStackItem.containerHelper.alignCenterVerticalConstraint?.constant = convertedPoint.y - horizontalStack.bounds.midY - } - return heroCenter - } - - public override func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { - if listItemModel?.action != nil { - super.didSelectCell(at: index, delegateObject: delegateObject, additionalData: additionalData) - } else { - heart.tapAction() - updateAccessibilityLabel() - } - } - open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 120 } open override func reset() { super.reset() - leftHeadline.setFontStyle(.BoldBodySmall) - leftBody.setFontStyle(.RegularBodySmall) + leftHeadline.setFontStyle(.BoldTitleSmall) + leftBody.setFontStyle(.RegularMicro) leftSubBody.setFontStyle(.RegularBodySmall) rightLabel.setFontStyle(.RegularBodySmall) + leftSubBody.textColor = UIColor.mfTextLightGray() } - //-------------------------------------------------- - // MARK: - Accessibility - //-------------------------------------------------- - - func getAccessibilityMessage() -> String? { - var message = "" - heart.updateAccessibilityLabel() - - if let leftHeadlineText = leftHeadline.text, !leftHeadlineText.isEmpty { - message += leftHeadlineText + ", " - } - - if let leftBodyText = leftBody.text, !leftBodyText.isEmpty { - message += leftBodyText + ", " - } - - if let leftSubBodyText = leftSubBody.text, !leftSubBodyText.isEmpty { - message += leftSubBodyText + ", " - } - - if let rightLabelText = rightLabel.text, !rightLabelText.isEmpty { - message += rightLabelText - } - return message.count > 0 ? message : nil - } - - func updateAccessibilityLabel() { - let hasHeart = !(horizontalStack.stackModel?.molecules[1].gone ?? true) - if let accessoryView = accessoryView, - hasHeart { - // Both accessory and heart actions. - isAccessibilityElement = false - accessoryView.accessibilityLabel = getAccessibilityMessage() - accessibilityElements = [accessoryView, heart] - } else { - // Make whole cell focusable if no action. - isAccessibilityElement = true - var message = getAccessibilityMessage() - if hasHeart { - accessibilityHint = heart.accessibilityHint - if let heartLabel = heart.accessibilityLabel { - message = (message ?? "") + ", " + heartLabel - } - } else { - accessibilityHint = nil - } - accessibilityLabel = message - } - } - - // Ensures voice over does not read "selected" after user triggers action on cell. override public var accessibilityTraits: UIAccessibilityTraits { get { if (accessoryView != nil) { return .button - } else if (!(horizontalStack.stackModel?.molecules[1].gone ?? true)) { - return heart.accessibilityTraits } else { return .none } } set { } } + + func updateAccessibilityLabel() { + isAccessibilityElement = true + let message = [model?.badge?.text, model?.leftHeadline.text, model?.leftBody.text, model?.leftSubBody.text, model?.rightLabel.text].compactMap { $0 }.joined(separator: ", ") + accessibilityLabel = message + } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocatorModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocatorModel.swift index 44c4ef90..912d8877 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocatorModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListStoreLocatorModel.swift @@ -12,7 +12,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol { //-------------------------------------------------- public static var identifier = "listStoreLocator" - public var heart: HeartModel? + public var badge: BadgeModel? public var leftHeadline: LabelModel public var leftBody: LabelModel public var leftSubBody: LabelModel @@ -22,8 +22,8 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol { // MARK: - Initializer //-------------------------------------------------- - public init(heart: HeartModel?, leftHeadline: LabelModel, leftBody: LabelModel, leftSubBody: LabelModel, rightLabel: LabelModel) { - self.heart = heart + public init(badge: BadgeModel?, leftHeadline: LabelModel, leftBody: LabelModel, leftSubBody: LabelModel, rightLabel: LabelModel) { + self.badge = badge self.leftHeadline = leftHeadline self.leftBody = leftBody self.leftSubBody = leftSubBody @@ -49,7 +49,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol { private enum CodingKeys: String, CodingKey { case moleculeName - case heart + case badge case leftHeadline case leftBody case leftSubBody @@ -62,7 +62,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol { public required init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - heart = try typeContainer.decodeIfPresent(HeartModel.self, forKey:.heart) + badge = try typeContainer.decodeIfPresent(BadgeModel.self, forKey:.badge) leftHeadline = try typeContainer.decode(LabelModel.self, forKey: .leftHeadline) leftBody = try typeContainer.decode(LabelModel.self, forKey: .leftBody) leftSubBody = try typeContainer.decode(LabelModel.self, forKey: .leftSubBody) @@ -74,7 +74,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) - try container.encodeIfPresent(heart, forKey: .heart) + try container.encodeIfPresent(badge, forKey: .badge) try container.encode(leftHeadline, forKey: .leftHeadline) try container.encode(leftBody, forKey: .leftBody) try container.encode(leftSubBody, forKey: .leftSubBody) diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index af356d93..b339cf94 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -72,6 +72,7 @@ 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: Badge.self, for: BadgeModel.self) // MARK:- Horizontal Combination Molecules ModelRegistry.register(handler: StringAndMoleculeView.self, for: StringAndMoleculeModel.self)