// // Badge.swift // VDS // // Created by Matt Bruce on 9/22/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// Badges are visual labels used to convey status or highlight supplemental information. @objc(VDSBadge) open class Badge: View { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum FillColor: String, CaseIterable { case red, yellow, green, orange, blue, black, white } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Label used to render text open var label = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.adjustsFontSizeToFitWidth = false $0.lineBreakMode = .byTruncatingTail $0.textPosition = .left $0.textStyle = .boldBodySmall } /// This will render the badges fill color based on the available options. /// When used in conjunction with the surface prop, this fill color will change its tint automatically based on a light or dark surface. open var fillColor: FillColor = .red { didSet { setNeedsUpdate() }} /// The text that will be shown in the label. open var text: String = "" { didSet { setNeedsUpdate() }} /// When applied, this property takes a px value that will restrict the width at that point. open var maxWidth: CGFloat? { didSet { setNeedsUpdate() }} /// This will restrict the badge height to a specific number of lines. If the text overflows the allowable space, ellipsis will show. open var numberOfLines: Int = 1 { didSet { setNeedsUpdate() }} //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var maxWidthConstraint: NSLayoutConstraint? private var minWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- private var minWidth: CGFloat = 23.0 private var labelInset: UIEdgeInsets = .init(top: 2, left: VDSLayout.Spacing.space1X.value, bottom: 2, right: VDSLayout.Spacing.space1X.value) /// ColorConfiguration that is mapped to the 'fillColor' for the surface. private var backgroundColorConfiguration: AnyColorable = { let config = KeyedColorConfiguration(keyPath: \.fillColor) config.setSurfaceColors(VDSColor.backgroundBrandhighlight, VDSColor.backgroundBrandhighlight, forKey: .red) config.setSurfaceColors(VDSColor.paletteYellow53, VDSColor.paletteYellow53, forKey: .yellow) config.setSurfaceColors(VDSColor.paletteGreen26, VDSColor.paletteGreen36, forKey: .green) config.setSurfaceColors(VDSColor.paletteOrange41, VDSColor.paletteOrange58, forKey: .orange) config.setSurfaceColors(VDSColor.paletteBlue38, VDSColor.paletteBlue46, forKey: .blue) config.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryDark, forKey: .black) config.setSurfaceColors(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryLight, forKey: .white) return config.eraseToAnyColorable() }() /// ColorConfiguration for the Text. private var textColorConfiguration = ViewColorConfiguration() /// Updates the textColorConfiguration based on the fillColor. public func updateTextColorConfig() { textColorConfiguration.reset() switch fillColor { case .red, .black: textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: true) case .yellow, .white: textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: true) case .orange, .green, .blue: textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forDisabled: true) } } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() accessibilityElements = [label] layer.cornerRadius = 2 addSubview(label) label.pinToSuperView(labelInset) maxWidthConstraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: 0) minWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth) minWidthConstraint?.isActive = true } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false label.reset() label.lineBreakMode = .byTruncatingTail label.textPosition = .left label.textStyle = .boldBodySmall fillColor = .red text = "" maxWidth = nil numberOfLines = 1 shouldUpdateView = true setNeedsUpdate() } /// Function used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateTextColorConfig() backgroundColor = backgroundColorConfiguration.getColor(self) label.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() label.numberOfLines = numberOfLines label.text = text label.surface = surface label.disabled = disabled if let maxWidth = maxWidth, maxWidth > minWidth { maxWidthConstraint?.constant = maxWidth maxWidthConstraint?.isActive = true minWidthConstraint?.isActive = false } else { maxWidthConstraint?.isActive = false minWidthConstraint?.isActive = true } } }