// // Badge.swift // VDS // // Created by Matt Bruce on 9/22/22. // import Foundation import UIKit import VDSCoreTokens import Combine /// A badge is a visual label used to convey status or highlight supplemental information. /// /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// to its parent this object will stretch to the parent's width. @objc(VDSBadge) open class Badge: View, ParentViewProtocol { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- /// Enum used to describe the primary color for the view. public enum FillColor: Equatable { case red, yellow, green, orange, blue, black, white case token(UIColor.VDSColor) case custom(UIColor) private var reflectedValue: String { String(reflecting: self) } public static func == (lhs: Self, rhs: Self) -> Bool { lhs.reflectedValue == rhs.reflectedValue } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var children: [any ViewProtocol] { [label] } /// Label used to render text open var label = Label().with { $0.isAccessibilityElement = false $0.lineBreakMode = .byTruncatingTail $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) $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() }} open var textColor: TextColor? { 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 func updateMaxWidth() { maxWidthConstraint?.isActive = false guard let maxWidth, maxWidth > minWidth else { return } maxWidthConstraint?.constant = maxWidth maxWidthConstraint?.isActive = true } //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- private var minWidth: CGFloat = 23.0 private var labelInset: UIEdgeInsets = .init(top: 2, left: VDSLayout.space1X, bottom: 2, right: VDSLayout.space1X) /// ColorConfiguration that is mapped to the 'fillColor' for the surface. private var backgroundColorConfiguration = SurfaceColorConfiguration() /// ColorConfiguration for the Text. private var textColorConfiguration = ViewColorConfiguration() /// Updates the textColorConfiguration based on the fillColor. public func updateColorConfig() { var config = backgroundColorConfiguration switch fillColor { case .red: config.lightColor = VDSColor.badgesBackgroundRedOnlight config.darkColor = VDSColor.badgesBackgroundRedOndark case .yellow: config.lightColor = VDSColor.badgesBackgroundYellowOnlight config.darkColor = VDSColor.badgesBackgroundYellowOndark case .green: config.lightColor = VDSColor.badgesBackgroundGreenOnlight config.darkColor = VDSColor.badgesBackgroundGreenOndark case .orange: config.lightColor = VDSColor.badgesBackgroundOrangeOnlight config.darkColor = VDSColor.badgesBackgroundOrangeOndark case .blue: config.lightColor = VDSColor.badgesBackgroundBlueOnlight config.darkColor = VDSColor.badgesBackgroundBlueOndark case .black: config.lightColor = VDSColor.badgesBackgroundBlackOnlight config.darkColor = VDSColor.badgesBackgroundBlackOndark case .white: config.lightColor = VDSColor.badgesBackgroundWhiteOnlight config.darkColor = VDSColor.badgesBackgroundWhiteOndark case .token(let color): config.lightColor = color.uiColor config.darkColor = color.uiColor case .custom(let color): config.lightColor = color config.darkColor = color } textColorConfiguration.reset() func update(for color: UIColor) { if let configuration = textColor?.configuration { textColorConfiguration = configuration } else { if color.isDark() { textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: true) } else { textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: true) } } } if let textColor { switch textColor { case .token(let color): textColorConfiguration.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: false) textColorConfiguration.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: true) case .custom(let color): textColorConfiguration.setSurfaceColors(color, color, forDisabled: false) textColorConfiguration.setSurfaceColors(color, color, forDisabled: true) } } else { 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) case .token(let color): update(for: color.uiColor) case .custom(let color): update(for: color) } } } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() isAccessibilityElement = true accessibilityTraits = .staticText layer.cornerRadius = 2 addSubview(label) label .pinTop(labelInset.top) .pinLeading(labelInset.left) .pinTrailing(labelInset.right) .pinBottom(labelInset.bottom, .defaultHigh) label.widthGreaterThanEqualTo(constant: minWidth) maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false } clipsToBounds = true } open override func setDefaults() { super.setDefaults() bridge_accessibilityLabelBlock = { [weak self] in guard let self else { return "" } return text } label.lineBreakMode = .byTruncatingTail label.textStyle = .boldBodySmall fillColor = .red text = "" maxWidth = nil numberOfLines = 1 } /// Resets to default settings. open override func reset() { label.reset() super.reset() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateColorConfig() updateMaxWidth() backgroundColor = backgroundColorConfiguration.getColor(self) label.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() label.numberOfLines = numberOfLines label.text = text label.surface = surface label.isEnabled = isEnabled } } extension Badge{ public enum TextColor: Equatable { case token(UIColor.VDSColor) case custom(UIColor) private var reflectedValue: String { String(reflecting: self) } public static func == (lhs: Self, rhs: Self) -> Bool { lhs.reflectedValue == rhs.reflectedValue } public var configuration: ViewColorConfiguration { let config = ViewColorConfiguration() switch self { case .token(let color): config.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: true) config.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: false) case .custom(let color): config.setSurfaceColors(color, color, forDisabled: true) config.setSurfaceColors(color, color, forDisabled: false) } return config } } }