diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 5e4a1b76..7ef890b9 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -40,6 +40,11 @@ EA4DB18528CA967F00103EE3 /* SelectorGroupHandlerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */; }; EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */; }; EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB30128DCBCA500103EE3 /* Badge.swift */; }; + EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E304B294CBDD00082B959 /* TileContainer.swift */; }; + EA5E304E294CC7F00082B959 /* VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E304D294CC7F00082B959 /* VDSColor.swift */; }; + EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E30522950DDA60082B959 /* TitleLockup.swift */; }; + EA5E3058295105A40082B959 /* Tilet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E3057295105A40082B959 /* Tilet.swift */; }; + EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E305929510F8B0082B959 /* EnumSubset.swift */; }; EA89200228AECF2A006B9984 /* UIButton+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200128AECF2A006B9984 /* UIButton+Publisher.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; EA89200628B526D6006B9984 /* CheckboxGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200528B526D6006B9984 /* CheckboxGroup.swift */; }; @@ -135,6 +140,11 @@ EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorGroupHandlerBase.swift; sourceTree = ""; }; EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEquatable.swift; sourceTree = ""; }; EA4DB30128DCBCA500103EE3 /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; + EA5E304B294CBDD00082B959 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = ""; }; + EA5E304D294CC7F00082B959 /* VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSColor.swift; sourceTree = ""; }; + EA5E30522950DDA60082B959 /* TitleLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockup.swift; sourceTree = ""; }; + EA5E3057295105A40082B959 /* Tilet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilet.swift; sourceTree = ""; }; + EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = ""; }; EA89200128AECF2A006B9984 /* UIButton+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Publisher.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; EA89200528B526D6006B9984 /* CheckboxGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxGroup.swift; sourceTree = ""; }; @@ -313,6 +323,9 @@ EAF7F11428A1470D00B287F5 /* RadioButton */, EA1F265F28B945070033E859 /* RadioSwatch */, EAC925852911C9DE00091998 /* TextFields */, + EA5E304A294CBDBB0082B959 /* TileContainer */, + EA5E3056295105930082B959 /* Tilet */, + EA5E30512950DD8D0082B959 /* TitleLockup */, EA3361A0288B1E6F0071C351 /* Toggle */, ); path = Components; @@ -337,6 +350,7 @@ EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */, EAB5FED329267EB300998C17 /* UIView.swift */, EAB5FF0029424ACB00998C17 /* UIControl.swift */, + EA5E304D294CC7F00082B959 /* VDSColor.swift */, ); path = Extensions; sourceTree = ""; @@ -349,6 +363,7 @@ EAA5EEDF28F49DB3003B3210 /* Colorable.swift */, EA3361AC288B26190071C351 /* DataTrackable.swift */, EA3361A9288B25E40071C351 /* Disabling.swift */, + EA5E305929510F8B0082B959 /* EnumSubset.swift */, EAF7F0A1289AFB3900B287F5 /* Errorable.swift */, EA3361AE288B26310071C351 /* FormFieldable.swift */, EA3361BE288B2EA60071C351 /* Handlerable.swift */, @@ -430,6 +445,30 @@ path = Badge; sourceTree = ""; }; + EA5E304A294CBDBB0082B959 /* TileContainer */ = { + isa = PBXGroup; + children = ( + EA5E304B294CBDD00082B959 /* TileContainer.swift */, + ); + path = TileContainer; + sourceTree = ""; + }; + EA5E30512950DD8D0082B959 /* TitleLockup */ = { + isa = PBXGroup; + children = ( + EA5E30522950DDA60082B959 /* TitleLockup.swift */, + ); + path = TitleLockup; + sourceTree = ""; + }; + EA5E3056295105930082B959 /* Tilet */ = { + isa = PBXGroup; + children = ( + EA5E3057295105A40082B959 /* Tilet.swift */, + ); + path = Tilet; + sourceTree = ""; + }; EA89200B28B530F0006B9984 /* RadioBox */ = { isa = PBXGroup; children = ( @@ -651,6 +690,7 @@ buildActionMask = 2147483647; files = ( EAF7F0B5289C126F00B287F5 /* UILabel.swift in Sources */, + EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */, EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, @@ -663,6 +703,7 @@ EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */, EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */, + EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, EA3361C5289030FC0071C351 /* Accessable.swift in Sources */, @@ -692,6 +733,8 @@ EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, + EA5E3058295105A40082B959 /* Tilet.swift in Sources */, + EA5E304E294CC7F00082B959 /* VDSColor.swift in Sources */, EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */, EAF7F09E289AAEC000B287F5 /* Constants.swift in Sources */, EA1F266528B945070033E859 /* RadioSwatch.swift in Sources */, @@ -700,6 +743,7 @@ EAF7F0AB289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA336171288B19200071C351 /* VDS.docc in Sources */, + EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */, EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */, EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */, EAB1D2E628AE842000DAE764 /* Publisher+Bind.swift in Sources */, diff --git a/VDS/Classes/Control.swift b/VDS/Classes/Control.swift index d5af7f46..ba2acd42 100644 --- a/VDS/Classes/Control.swift +++ b/VDS/Classes/Control.swift @@ -29,10 +29,12 @@ open class Control: UIControl, Handlerable, ViewProtocol, Resettable { open override var isSelected: Bool { didSet { didChange() } } + internal var enabledHighlight: Bool = true + var isHighlightAnimating = false open override var isHighlighted: Bool { didSet { - if isHighlightAnimating == false { + if isHighlightAnimating == false && enabledHighlight { isHighlightAnimating = true UIView.animate(withDuration: 0.1, animations: { [weak self] in self?.updateView() diff --git a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift index 9929da63..420302e9 100644 --- a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift +++ b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift @@ -124,6 +124,10 @@ public class RadioSwatchGroupBase: SelectorGroupSe collectionView.reloadData() } + public func reload() { + collectionView.reloadData() + } + //-------------------------------------------------- // MARK: - UICollectionViewDelegateFlowLayout //-------------------------------------------------- diff --git a/VDS/Components/TextFields/EntryField/EntryField.swift b/VDS/Components/TextFields/EntryField/EntryField.swift index d38d4453..851fb0e5 100644 --- a/VDS/Components/TextFields/EntryField/EntryField.swift +++ b/VDS/Components/TextFields/EntryField/EntryField.swift @@ -163,10 +163,10 @@ open class EntryField: Control, Accessable { open override func setup() { super.setup() + enabledHighlight = false isAccessibilityElement = true accessibilityTraits = .button addSubview(stackView) - stackView.isUserInteractionEnabled = false //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) @@ -184,8 +184,12 @@ open class EntryField: Control, Accessable { stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) - stackView.pinToSuperView() - + stackView + .pinTop() + .pinBottom() + .pinLeading() + .trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor).isActive = true + titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift new file mode 100644 index 00000000..1f676421 --- /dev/null +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -0,0 +1,316 @@ +// +// TileContainer.swift +// VDS +// +// Created by Matt Bruce on 12/16/22. +// + +import Foundation +import VDSColorTokens +import UIKit + +@objc(VDSTileContainer) +open class TileContainer: Control { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + initialSetup() + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + public enum ContainerBackgroundColor: String, CaseIterable { + case white + case black + case gray + case transparent + } + + public enum ContainerPadding: String, CaseIterable { + case twelve = "12" + case sixteen = "16" + case twentyFour = "24" + case thirtyTwo = "32" + case fourtyEight = "48" + } + + public enum ContainerScalingType: String, CaseIterable { + case ratio1x1 = "1:1" + case ratio3x4 = "3:4" + case ratio4x3 = "4:3" + case ratio2x3 = "2:3" + case ratio3x2 = "3:2" + case ratio9x16 = "9:16" + case ratio16x9 = "16:9" + case ratio1x2 = "1:2" + case ratio2x1 = "2:1" + case none + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var backgroundImage: UIImage? { didSet{ didChange() } } + + public var containerView = View() + + public var highlightView = View() + + public var containerBackgroundColor: ContainerBackgroundColor = .white { didSet{ didChange() } } + + public var containerPadding: ContainerPadding = .sixteen { didSet{ didChange() } } + + public var aspectRatio: ContainerScalingType = .ratio1x1 { didSet{ didChange() } } + + public var imageFallbackColor: Surface = .light { didSet{ didChange() } } + + private var _width: CGFloat = 100 + public var width: CGFloat { + get { return _width } + set { + if newValue > 100 { + _width = newValue + didChange() + } + } + } + + private var _height: CGFloat? + public var height: CGFloat? { + get { return _height } + set { + if let newValue, newValue > 44 { + _height = newValue + } else { + _height = nil + } + didChange() + } + } + + public var showBorder: Bool = false { didSet{ didChange() } } + + public var showDropShadows: Bool = false { didSet{ didChange() } } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var backgroundImageView = UIImageView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + } + + internal var padding: CGFloat { + switch containerPadding { + case .twelve: + return 12.0 + case .sixteen: + return 16.0 + case .twentyFour: + return 24.0 + case .thirtyTwo: + return 32.0 + case .fourtyEight: + return 48.0 + } + } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + internal var widthConstraint: NSLayoutConstraint? + internal var heightConstraint: NSLayoutConstraint? + internal var heightGreaterThanConstraint: NSLayoutConstraint? + internal var containerTopConstraint: NSLayoutConstraint? + internal var containerBottomConstraint: NSLayoutConstraint? + internal var containerLeadingConstraint: NSLayoutConstraint? + internal var containerTrailingConstraint: NSLayoutConstraint? + + //functions + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func setup() { + super.setup() + addSubview(backgroundImageView) + addSubview(containerView) + addSubview(highlightView) + + widthConstraint = widthAnchor.constraint(equalToConstant: width) + widthConstraint?.isActive = true + + heightGreaterThanConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + heightGreaterThanConstraint?.isActive = false + + heightConstraint = heightAnchor.constraint(equalToConstant: width) + heightConstraint?.isActive = true + + backgroundImageView.pinToSuperView() + backgroundImageView.isUserInteractionEnabled = false + backgroundImageView.isHidden = true + + containerView.isUserInteractionEnabled = false + containerView.backgroundColor = .clear + + containerTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: padding) + containerTopConstraint?.isActive = true + containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: padding) + containerBottomConstraint?.isActive = true + containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding) + containerLeadingConstraint?.isActive = true + containerTrailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding) + containerTrailingConstraint?.isActive = true + + highlightView.pinToSuperView() + highlightView.isUserInteractionEnabled = false + highlightView.isHidden = true + highlightView.backgroundColor = .clear + + //corner radius + layer.cornerRadius = cornerRadius + backgroundImageView.layer.cornerRadius = 8 + highlightView.layer.cornerRadius = 8 + + } + + public override func reset() { + super.reset() + + } + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let cornerRadius = 8.0 + + private var backgroundColorConfig = BackgroundColorConfiguration() + + private var borderColorConfig = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.elementsLowContrastOnLight + $0.darkColor = VDSColor.elementsLowContrastOnDark + } + + private var imageFallbackColorConfig = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.backgroundPrimaryLight + $0.darkColor = VDSColor.backgroundPrimaryDark + } + + private var hightLightViewColorConfig = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.paletteWhite.withAlphaComponent(0.3) + $0.darkColor = VDSColor.paletteBlack.withAlphaComponent(0.3) + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + var ratioSize: CGSize { + var height: CGFloat = width + + switch aspectRatio { + case .ratio1x1: + break; + case .ratio3x4: + height = (4 / 3) * width + case .ratio4x3: + height = (3 / 4) * width + case .ratio2x3: + height = (3 / 2) * width + case .ratio3x2: + height = (2 / 3) * width + case .ratio9x16: + height = (16 / 9) * width + case .ratio16x9: + height = (9 / 16) * width + case .ratio1x2: + height = (2 / 1) * width + case .ratio2x1: + height = (1 / 2) * width + + default: + break + } + + return CGSize(width: width, height: height) + } + + open override func updateView() { + super.updateView() + + highlightView.backgroundColor = hightLightViewColorConfig.getColor(self) + highlightView.isHidden = !isHighlighted + + if let backgroundImage { + backgroundImageView.image = backgroundImage + backgroundImageView.isHidden = false + backgroundColor = imageFallbackColorConfig.getColor(self) + } else { + backgroundImageView.isHidden = true + backgroundColor = backgroundColorConfig.getColor(self) + } + + layer.borderColor = borderColorConfig.getColor(self).cgColor + layer.borderWidth = showBorder ? 1 : 0 + + containerTopConstraint?.constant = padding + containerLeadingConstraint?.constant = padding + containerBottomConstraint?.constant = -padding + containerTrailingConstraint?.constant = -padding + + if aspectRatio == .none { + widthConstraint?.constant = width + heightConstraint?.isActive = false + heightGreaterThanConstraint?.isActive = true + } else if let height { + widthConstraint?.constant = width + heightConstraint?.constant = height + heightConstraint?.isActive = true + heightGreaterThanConstraint?.isActive = false + } else { + let size = ratioSize + widthConstraint?.constant = size.width + heightConstraint?.constant = size.height + heightConstraint?.isActive = true + heightGreaterThanConstraint?.isActive = false + } + } + + public func addContentView(_ view: UIView, shouldPin: Bool = true) { + containerView.addSubview(view) + if shouldPin { + view.pinToSuperView() + } + } + + class BackgroundColorConfiguration: ObjectColorable { + typealias ObjectType = TileContainer + + required init() { } + + func getColor(_ object: TileContainer) -> UIColor { + switch object.containerBackgroundColor { + + case .white: + return VDSColor.backgroundPrimaryLight + case .black: + return VDSColor.backgroundPrimaryDark + case .gray: + return VDSColor.backgroundSecondaryLight + case .transparent: + return UIColor.clear + } + } + } +} diff --git a/VDS/Components/Tilet/Tilet.swift b/VDS/Components/Tilet/Tilet.swift new file mode 100644 index 00000000..417a7f08 --- /dev/null +++ b/VDS/Components/Tilet/Tilet.swift @@ -0,0 +1,208 @@ +// +// Tilet.swift +// VDS +// +// Created by Matt Bruce on 12/19/22. +// + +import Foundation +import Foundation +import VDSColorTokens +import UIKit + +public enum TiletTitleTypographicalStyle: String, Codable, EnumSubset { + case TitleXLarge + case BoldTitleXLarge + case TitleLarge + case BoldTitleLarge + case TitleMedium + case BoldTitleMedium + case TitleSmall + case BoldTitleSmall + + public var defaultValue: TitleLockupTitleTypographicalStyle { .BoldTitleSmall } +} + +public enum TiletOtherTypographicalStyle: String, Codable, EnumSubset { + case BodyLarge + case BoldBodyLarge + case BodyMedium + case BoldBodyMedium + case BodySmall + case BoldBodySmall + + public var defaultValue: TitleLockupOtherTypographicalStyle { .BodySmall } +} + + +@objc(VDSTilet) +open class Tilet: View { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + initialSetup() + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var tileContainer = TileContainer().with { + $0.aspectRatio = .none + $0.surface = .light + } + + private var titleLockup = TitleLockup() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + //style + open var titleTypograpicalStyle: TiletTitleTypographicalStyle = .BoldTitleSmall { didSet { didChange() }} + open var otherTypograpicalStyle: TiletOtherTypographicalStyle = .BodySmall { didSet { didChange() }} + + open var width: CGFloat = 100 { didSet { didChange() }} + + private var _textWidth: CGFloat? + open var textWidth: CGFloat? { + get { _textWidth } + set { + if let newValue, newValue > 44.0 && newValue <= width { + _textWidth = newValue + if _textPercentage != nil { + _textPercentage = nil + } + } else { + _textWidth = nil + } + didChange() + } + + } + + private var _textPercentage: CGFloat? + open var textPercentage: CGFloat? { + get { _textPercentage } + set { + if let newValue, newValue >= 5 && newValue <= 100.0 { + _textPercentage = newValue + if textWidth != nil { + _textWidth = nil + } + } else { + _textPercentage = nil + } + didChange() + } + } + + //text + open var titleText: String = "" { didSet { didChange() }} + open var titleTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} + + open var subTitleText: String = "" { didSet { didChange() }} + open var subTitleTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} + + open var subTitleColor: Use = .primary { didSet { didChange() }} + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + internal var titleLockupWidthConstraint: NSLayoutConstraint? + internal var titleLockupTrailingConstraint: NSLayoutConstraint? + //functions + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func setup() { + super.setup() + addSubview(tileContainer) + tileContainer.pinToSuperView() + tileContainer.addContentView(titleLockup, shouldPin: false) + titleLockup.pinTop() + titleLockup.pinLeading() + titleLockup.pinBottom() + + //either you are 100% width of the tileContainer.contentView + titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: tileContainer.containerView.trailingAnchor) + titleLockupTrailingConstraint?.isActive = true + } + + public override func reset() { + super.reset() + tileContainer.reset() + tileContainer.aspectRatio = .none + tileContainer.surface = .light + + titleLockup.reset() + + titleText = "" + titleTextAttributes = nil + subTitleText = "" + subTitleTextAttributes = nil + subTitleColor = .primary + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + + open override func updateView() { + super.updateView() + + //flip the color + let flippedColor:TileContainer.ContainerBackgroundColor = surface == .dark ? .white : .black + tileContainer.containerBackgroundColor = flippedColor + tileContainer.width = width + + //flip the surface for the titleLockup + let flippedSurface: Surface = surface == .dark ? .light : .dark + titleLockup.surface = flippedSurface + + //either use textWidth + if let textWidth { + titleLockupTrailingConstraint?.isActive = false + titleLockupWidthConstraint?.isActive = false + titleLockupWidthConstraint = titleLockup.widthAnchor.constraint(equalToConstant: textWidth) + titleLockupWidthConstraint?.isActive = true + + } else if let textPercentage { + titleLockupTrailingConstraint?.isActive = false + titleLockupWidthConstraint?.isActive = false + titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup, + attribute: .width, + relatedBy: .equal, + toItem: tileContainer.containerView, + attribute: .width, + multiplier: textPercentage / 100, + constant: 0.0) + titleLockupWidthConstraint?.isActive = true + + } else { + titleLockupWidthConstraint?.isActive = false + titleLockupTrailingConstraint?.isActive = true + + } + + titleLockup.titleText = titleText + titleLockup.titleTypograpicalStyle = titleTypograpicalStyle.value + titleLockup.titleTextAttributes = titleTextAttributes + + titleLockup.subTitleText = subTitleText + titleLockup.otherTypograpicalStyle = otherTypograpicalStyle.value + titleLockup.subTitleTextAttributes = titleTextAttributes + titleLockup.subTitleColor = subTitleColor + } +} diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift new file mode 100644 index 00000000..958d7a81 --- /dev/null +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -0,0 +1,337 @@ +// +// TitleLockup.swift +// VDS +// +// Created by Matt Bruce on 12/19/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import Combine + +public enum TitleLockupTextPosition: String, Codable, CaseIterable { + case left, center + + var labelTextPosition: TextPosition { + switch self { + case .left: + return .left + case .center: + return .center + } + } +} + +public enum TitleLockupTitleTypographicalStyle: String, Codable, EnumSubset { + + case FeatureMedium + case BoldFeatureMedium + case FeatureSmall + case BoldFeatureSmall + case FeatureXSmall + case BoldFeatureXSmall + + case Title2XLarge + case BoldTitle2XLarge + case TitleXLarge + case BoldTitleXLarge + case TitleLarge + case BoldTitleLarge + case TitleMedium + case BoldTitleMedium + case TitleSmall + case BoldTitleSmall + + public var defaultValue: TypographicalStyle {.BoldFeatureXSmall } +} + +public enum TitleLockupOtherTypographicalStyle: String, Codable, EnumSubset { + case BodyLarge + case BoldBodyLarge + case BodyMedium + case BoldBodyMedium + case BodySmall + case BoldBodySmall + + public var defaultValue: TypographicalStyle {.BodyLarge } +} + +@objc(VDSTitleLockup) +open class TitleLockup: View { + + //-------------------------------------------------- + // 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: - Private Properties + //-------------------------------------------------- + private var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + open var topTypographicalStyleSpacingConfig: TypographicalStyleSpacingConfig = { + let configs = [ + TypographicalStyleDeviceSpacingConfig([.BoldTitleLarge, .TitleLarge], + neighboring: [.BodySmall, .BodyMedium, .BodyLarge], + spacing: 12.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitleXLarge, .TitleXLarge], + neighboring: [.TitleMedium, .BodyLarge], + spacing: 12.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.TitleMedium, .TitleLarge], + spacing: 16.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.BodyLarge], + spacing: 12.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall, .BoldFeatureMedium, .FeatureMedium], + neighboring: [.TitleMedium, .TitleLarge], + spacing: 16.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall, .BoldFeatureMedium, .FeatureMedium], + neighboring: [.BodyLarge], + spacing: 12.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitleXLarge, .TitleXLarge], + neighboring: [.BodyLarge, .BodyMedium, .BodySmall, .TitleMedium], + spacing: 12.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.BodyLarge, .BodyMedium, .TitleMedium], + spacing: 12.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall], + neighboring: [.TitleLarge, .BodyLarge], + spacing: 12.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureMedium, .FeatureMedium], + neighboring: [.TitleLarge, .TitleXLarge], + spacing: 16.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureMedium, .FeatureMedium], + neighboring: [.BodyLarge], + spacing: 12.0, + deviceType: .iPhone) + ] + return TypographicalStyleSpacingConfig(configs: configs) + }() + + open var bottomTypographicalStyleSpacingConfig: TypographicalStyleSpacingConfig = { + let configs = [ + TypographicalStyleDeviceSpacingConfig([.BoldTitleLarge, .TitleLarge], + neighboring: [.BodySmall, .BodyMedium, .BodyLarge], + spacing: 12.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitleXLarge, .TitleXLarge], + neighboring: [.TitleMedium, .BodyLarge], + spacing: 16.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.TitleMedium, .TitleLarge], + spacing: 24.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.BodyLarge], + spacing: 24.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall, .BoldFeatureMedium, .FeatureMedium], + neighboring: [.TitleMedium, .TitleLarge], + spacing: 24.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall, .BoldFeatureMedium, .FeatureMedium], + neighboring: [.BodyLarge], + spacing: 24.0, + deviceType: .iPad), + + TypographicalStyleDeviceSpacingConfig([.BoldTitleXLarge, .TitleXLarge], + neighboring: [.BodyLarge, .BodyMedium, .BodySmall, .TitleMedium], + spacing: 12.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldTitle2XLarge, .Title2XLarge, .BoldFeatureXSmall, .FeatureXSmall], + neighboring: [.BodyLarge, .BodyMedium, .TitleMedium], + spacing: 16, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureSmall, .FeatureSmall], + neighboring: [.TitleLarge, .BodyLarge], + spacing: 16.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureMedium, .FeatureMedium], + neighboring: [.TitleLarge, .TitleXLarge], + spacing: 24.0, + deviceType: .iPhone), + + TypographicalStyleDeviceSpacingConfig([.BoldFeatureMedium, .FeatureMedium], + neighboring: [.BodyLarge], + spacing: 24.0, + deviceType: .iPhone) + ] + return TypographicalStyleSpacingConfig(configs: configs) + }() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var textPosition: TitleLockupTextPosition = .left { didSet { didChange() }} + + //style + open var titleTypograpicalStyle: TitleLockupTitleTypographicalStyle = .BoldFeatureXSmall { didSet { didChange() }} + open var otherTypograpicalStyle: TitleLockupOtherTypographicalStyle = UIDevice.isIPad ? .BodyLarge : .BodyMedium { didSet { didChange() }} + + //first row + open var eyebrowLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + open var eyebrowText: String = "" { didSet { didChange() }} + open var eyebrowTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} + + //second row + open var titleLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + open var titleText: String = "" { didSet { didChange() }} + open var titleTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} + + //third row + open var subTitleLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + open var subTitleText: String = "" { didSet { didChange() }} + open var subTitleTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} + open var subTitleColor: Use = .primary { didSet { didChange() }} + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func setup() { + super.setup() + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(stackView) + + stackView.spacing = 0.0 + + stackView.addArrangedSubview(eyebrowLabel) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subTitleLabel) + + //pin stackview to edges + stackView.pinToSuperView() + } + + public override func reset() { + super.reset() + titleLabel.reset() + eyebrowLabel.reset() + subTitleLabel.reset() + + textPosition = .left + + eyebrowText = "" + eyebrowTextAttributes = nil + titleText = "" + titleTextAttributes = nil + subTitleText = "" + subTitleTextAttributes = nil + titleTextAttributes = nil + titleTypograpicalStyle = .BoldFeatureXSmall + otherTypograpicalStyle = .BodyLarge + + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + open override func updateView() { + super.updateView() + + let allLabelsTextPosition = textPosition.labelTextPosition + + eyebrowLabel.textPosition = allLabelsTextPosition + eyebrowLabel.typograpicalStyle = otherTypograpicalStyle.value + eyebrowLabel.text = eyebrowText + eyebrowLabel.attributes = eyebrowTextAttributes + eyebrowLabel.surface = surface + + titleLabel.textPosition = allLabelsTextPosition + titleLabel.typograpicalStyle = titleTypograpicalStyle.value + titleLabel.text = titleText + titleLabel.attributes = titleTextAttributes + titleLabel.surface = surface + + subTitleLabel.textPosition = allLabelsTextPosition + subTitleLabel.typograpicalStyle = otherTypograpicalStyle.value + subTitleLabel.text = subTitleText + subTitleLabel.attributes = subTitleTextAttributes + subTitleLabel.surface = surface + subTitleLabel.disabled = subTitleColor == .secondary + + //if both first 2 rows not empty set spacing + if !eyebrowText.isEmpty && !titleText.isEmpty { + stackView.spacing = getTopSpacing() + } else { + stackView.spacing = 0.0 + } + + //if either first 2 rows not empty and subtile not empty, create space else collapse + if (!eyebrowText.isEmpty || !titleText.isEmpty) && !subTitleText.isEmpty { + stackView.setCustomSpacing(getBottomSpacing(), after: titleLabel) + } else if (!eyebrowText.isEmpty || !titleText.isEmpty) && subTitleText.isEmpty { + stackView.setCustomSpacing(0.0, after: titleLabel) + } + } + + open func getTopSpacing() -> CGFloat { + topTypographicalStyleSpacingConfig.spacing(for: titleTypograpicalStyle.value, neighboring: otherTypograpicalStyle.value) + } + + open func getBottomSpacing() -> CGFloat { + bottomTypographicalStyleSpacingConfig.spacing(for: titleTypograpicalStyle.value, neighboring: otherTypograpicalStyle.value) + } +} + +extension TypographicalStyle { + func isWithin(_ collection: [TypographicalStyle]) -> Bool { + (collection.first(where: {$0 == self}) != nil) + } +} + diff --git a/VDS/Extensions/VDSColor.swift b/VDS/Extensions/VDSColor.swift new file mode 100644 index 00000000..d1b7cffe --- /dev/null +++ b/VDS/Extensions/VDSColor.swift @@ -0,0 +1,15 @@ +// +// VDSColor.swift +// VDS +// +// Created by Matt Bruce on 12/16/22. +// + +import Foundation +import VDSColorTokens +import UIKit + +extension VDSColor { + public static let elementsLowContrastOnLight = UIColor.init(hexString: "#D8DADA") + public static let elementsLowContrastOnDark = UIColor.init(hexString: "#333333") +} diff --git a/VDS/Protocols/EnumSubset.swift b/VDS/Protocols/EnumSubset.swift new file mode 100644 index 00000000..5f92a1cd --- /dev/null +++ b/VDS/Protocols/EnumSubset.swift @@ -0,0 +1,19 @@ +// +// EnumSubset.swift +// VDS +// +// Created by Matt Bruce on 12/19/22. +// + +import Foundation + +public protocol EnumSubset: RawRepresentable, CaseIterable { + associatedtype T:RawRepresentable + var defaultValue: T { get } +} + +extension EnumSubset where RawValue == T.RawValue { + public var value: T { + T(rawValue: rawValue) ?? defaultValue + } +} diff --git a/VDS/Typography/Typography.swift b/VDS/Typography/Typography.swift index 86629d91..daecf91b 100644 --- a/VDS/Typography/Typography.swift +++ b/VDS/Typography/Typography.swift @@ -281,3 +281,36 @@ extension TypographicalStyle { } } } + +public struct TypographicalStyleSpacingConfig { + public var defaultSpacing: CGFloat = 8.0 + public var configs: [TypographicalStyleDeviceSpacingConfig] + + public func spacing(for style: TypographicalStyle, neighboring: TypographicalStyle) -> CGFloat { + let deviceType: TypographicalStyleDeviceSpacingConfig.DeviceType = UIDevice.isIPad ? .iPad : .iPhone + if let config = configs.first(where: + { style.isWithin($0.primaryStyles) && neighboring.isWithin($0.neighboringStyles) && + ($0.deviceType == deviceType || $0.deviceType == .all )}) + { + return config.spacing + } + return defaultSpacing + } +} + +public struct TypographicalStyleDeviceSpacingConfig { + public enum DeviceType { + case iPhone, iPad, all + } + public var spacing: CGFloat + public var deviceType: DeviceType = .iPhone + public var primaryStyles: [TypographicalStyle] + public var neighboringStyles: [TypographicalStyle] + + public init(_ primaryStyles: [TypographicalStyle], neighboring: [TypographicalStyle], spacing: CGFloat, deviceType: DeviceType = .iPhone) { + self.spacing = spacing + self.primaryStyles = primaryStyles + self.neighboringStyles = neighboring + self.deviceType = deviceType + } +}