diff --git a/VDS/Components/BadgeIndicator/BadgeIndicator.swift b/VDS/Components/BadgeIndicator/BadgeIndicator.swift index 68af0d22..511f9b61 100644 --- a/VDS/Components/BadgeIndicator/BadgeIndicator.swift +++ b/VDS/Components/BadgeIndicator/BadgeIndicator.swift @@ -10,6 +10,7 @@ import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine +import VDSTypographyTokens /// Badges are visual labels used to convey status or highlight supplemental information. @objc(VDSBadgeIndicator) @@ -21,6 +22,104 @@ open class BadgeIndicator: View { case red, yellow, green, orange, blue, gray, grayLowContrast, black, white } + public enum Kind: String, CaseIterable { + case simple, numbered + } + + public enum MaxDigits: String, CaseIterable { + case one + case two + case three + case four + case five + case six + + public var value: Int { + switch self { + case .two: + return 2 + case .one: + return 1 + case .three: + return 3 + case .four: + return 4 + case .five: + return 5 + case .six: + return 6 + } + } + } + + public enum TextSize: String, CaseIterable { + case xxlarge = "2XLarge" + case xlarge = "XLarge" + case large = "Large" + case medium = "Medium" + case small = "Small" + + public var minimumSize: CGFloat { + switch self { + case .xxlarge: + return 29 + case .xlarge: + return 24 + case .large: + return 20 + case .medium: + return 18 + case .small: + return 16 + } + } + + public var padding: CGFloat { + switch self { + case .xxlarge: + return 8 + case .xlarge: + return 6 + case .large: + return 6 + case .medium: + return 6 + case .small: + return 4 + } + } + + public var textStyle: TextStyle { + let style = TextStyle.bodySmall + var pointSize: CGFloat = VDSTypography.fontSizeBody12 + + switch self { + case .xxlarge: + pointSize = VDSTypography.fontSizeTitle24 + + case .xlarge: + pointSize = VDSTypography.fontSizeTitle20 + + case .large: + pointSize = VDSTypography.fontSizeBody16 + + case .medium: + pointSize = VDSTypography.fontSizeBody14 + + case .small: + pointSize = VDSTypography.fontSizeBody12 + + } + + return TextStyle(rawValue: "\(self.rawValue)BadgeIndicator", + fontFace: style.fontFace, + pointSize: pointSize, + lineHeight: style.lineHeight, + letterSpacing: style.letterSpacing) + } + + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -28,24 +127,54 @@ open class BadgeIndicator: View { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.adjustsFontSizeToFitWidth = false $0.lineBreakMode = .byTruncatingTail - $0.textPosition = .left - $0.textStyle = .boldBodySmall + $0.textPosition = .center + $0.numberOfLines = 1 + } + + open var borderColorLight: UIColor? { + didSet { + if let borderColorLight { + borderColorConfiguration.lightColor = borderColorLight + } else { + borderColorConfiguration.lightColor = VDSColor.paletteWhite + } + setNeedsUpdate() + } + } + + open var borderColorDark: UIColor? { + didSet { + if let borderColorDark { + borderColorConfiguration.darkColor = borderColorDark + } else { + borderColorConfiguration.darkColor = VDSColor.paletteBlack + } + setNeedsUpdate() + } } open var fillColor: FillColor = .red { didSet { setNeedsUpdate() }} - open var text: String = "" { didSet { setNeedsUpdate() }} + open var number: Int? { didSet { setNeedsUpdate() }} - open var maxWidth: CGFloat? { didSet { setNeedsUpdate() }} + open var kind: Kind = .simple { didSet { setNeedsUpdate() }} + + open var leadingCharacter: String? { didSet { setNeedsUpdate() }} - open var numberOfLines: Int = 1 { didSet { setNeedsUpdate() }} - + open var textSize: TextSize = .xxlarge { didSet { setNeedsUpdate() }} + + open var hideDot: Bool = false { didSet { setNeedsUpdate() }} + + open var hideBorder: Bool = false { didSet { setNeedsUpdate() }} + + open var maxDigits: MaxDigits = .two { didSet { setNeedsUpdate() }} + //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- - private var maxWidthConstraint: NSLayoutConstraint? - private var minWidthConstraint: NSLayoutConstraint? - + private var labelWidthConstraint: NSLayoutConstraint? + private var labelHeightConstraint: NSLayoutConstraint? + private var defaultBadgeSize: CGFloat = 16 //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -54,18 +183,17 @@ open class BadgeIndicator: View { super.setup() accessibilityElements = [label] - layer.cornerRadius = 2 - + addSubview(label) - label.pinToSuperView(.init(top: 2, - left: VDSLayout.Spacing.space1X.value, - bottom: 2, - right: VDSLayout.Spacing.space1X.value)) - - maxWidthConstraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: 100) - minWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 23) - minWidthConstraint?.isActive = true + label.pinToSuperView() + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: centerXAnchor), + label.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + + labelWidthConstraint = label.widthGreaterThanEqualTo(constant: defaultBadgeSize).activate() + labelHeightConstraint = label.heightGreaterThanEqualTo(constant: defaultBadgeSize).activate() } open override func reset() { @@ -73,12 +201,9 @@ open class BadgeIndicator: View { shouldUpdateView = false label.reset() label.lineBreakMode = .byTruncatingTail - label.textPosition = .left - label.textStyle = .boldBodySmall + label.textPosition = .center fillColor = .red - text = "" - maxWidth = nil - numberOfLines = 1 + number = nil shouldUpdateView = true setNeedsUpdate() } @@ -86,6 +211,8 @@ open class BadgeIndicator: View { //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- + private var borderColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteBlack) + private var backgroundColorConfiguration: AnyColorable = { let config = KeyedColorConfiguration(keyPath: \.fillColor) config.setSurfaceColors(VDSColor.backgroundBrandhighlight, VDSColor.backgroundBrandhighlight, forKey: .red) @@ -93,6 +220,8 @@ open class BadgeIndicator: View { 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.paletteGray44, VDSColor.paletteGray65, forKey: .gray) + config.setSurfaceColors(VDSColor.paletteGray85, VDSColor.paletteGray20, forKey: .grayLowContrast) config.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryDark, forKey: .black) config.setSurfaceColors(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryLight, forKey: .white) return config.eraseToAnyColorable() @@ -105,7 +234,7 @@ open class BadgeIndicator: View { switch fillColor { - case .red, .black: + case .red, .black, .gray, .grayLowContrast: textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: false) textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: true) @@ -126,18 +255,71 @@ open class BadgeIndicator: View { updateTextColorConfig() backgroundColor = backgroundColorConfiguration.getColor(self) - + + label.useAttributedText = true + label.edgeInset = .init(top: 0, left: textSize.padding, bottom: 0, right: textSize.padding) + label.font = textSize.textStyle.font + label.textColor = textColorConfiguration.getColor(self) label.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() - label.numberOfLines = numberOfLines - label.text = text + label.text = getText() label.surface = surface label.disabled = disabled + label.sizeToFit() + setNeedsLayout() + layoutIfNeeded() + } + + private func getText() -> String { + let badgeCount = number ?? 0 + var text: String = "" + if kind == .numbered { + let maxBadgetCount = limitDigits(number: badgeCount, maxDigits: maxDigits.value) + + text = "\(maxBadgetCount)" + if maxDigits.value < "\(badgeCount)".count { + text = "\(maxBadgetCount)+" + } + if let leadingCharacter { + text = "\(leadingCharacter)\(text)" + } else { + text = "\(text)" + } + } + return text + } + + private func limitDigits(number: Int, maxDigits: Int) -> Int { + let maxNumber = Int(pow(10.0, Double(maxDigits))) - 1 + return min(number, maxNumber) + } + + open override func layoutSubviews() { + super.layoutSubviews() + labelWidthConstraint?.constant = textSize.minimumSize + labelHeightConstraint?.constant = textSize.minimumSize + layer.cornerRadius = frame.size.height / 2 - if let maxWidth = maxWidth, let minWidth = minWidthConstraint?.constant, maxWidth > minWidth { - maxWidthConstraint?.constant = maxWidth - maxWidthConstraint?.isActive = true + if hideBorder { + layer.borderWidth = 0 } else { - maxWidthConstraint?.isActive = false + layer.borderColor = borderColorConfiguration.getColor(surface).cgColor + layer.borderWidth = 1 + } + + layer.remove(layerName: "dot") + if kind == .simple && !hideDot { + let dotSize: CGFloat = bounds.width * 0.1875 + let dotLayer = CAShapeLayer() + dotLayer.name = "dot" + + let centerX = (bounds.width - dotSize) / 2.0 + let centerY = (bounds.width - dotSize) / 2.0 + + dotLayer.frame = .init(x: centerX, y: centerY, width: dotSize, height: dotSize) + dotLayer.path = UIBezierPath(ovalIn: .init(origin: .zero, size: .init(width: dotSize, height: dotSize))).cgPath + dotLayer.fillColor = textColorConfiguration.getColor(self).cgColor + + layer.addSublayer(dotLayer) } } }