Merge branch 'feature/tilet' into 'develop'

added tileContainer and extension for VDSColor

See merge request BPHV_MIPS/vds_ios!22
This commit is contained in:
Bruce, Matt R 2022-12-21 16:21:55 +00:00
commit 9a8d512403
10 changed files with 986 additions and 4 deletions

View File

@ -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 = "<group>"; };
EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEquatable.swift; sourceTree = "<group>"; };
EA4DB30128DCBCA500103EE3 /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = "<group>"; };
EA5E304B294CBDD00082B959 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = "<group>"; };
EA5E304D294CC7F00082B959 /* VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSColor.swift; sourceTree = "<group>"; };
EA5E30522950DDA60082B959 /* TitleLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockup.swift; sourceTree = "<group>"; };
EA5E3057295105A40082B959 /* Tilet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilet.swift; sourceTree = "<group>"; };
EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = "<group>"; };
EA89200128AECF2A006B9984 /* UIButton+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Publisher.swift"; sourceTree = "<group>"; };
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = "<group>"; };
EA89200528B526D6006B9984 /* CheckboxGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxGroup.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
};
EA5E304A294CBDBB0082B959 /* TileContainer */ = {
isa = PBXGroup;
children = (
EA5E304B294CBDD00082B959 /* TileContainer.swift */,
);
path = TileContainer;
sourceTree = "<group>";
};
EA5E30512950DD8D0082B959 /* TitleLockup */ = {
isa = PBXGroup;
children = (
EA5E30522950DDA60082B959 /* TitleLockup.swift */,
);
path = TitleLockup;
sourceTree = "<group>";
};
EA5E3056295105930082B959 /* Tilet */ = {
isa = PBXGroup;
children = (
EA5E3057295105A40082B959 /* Tilet.swift */,
);
path = Tilet;
sourceTree = "<group>";
};
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 */,

View File

@ -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()

View File

@ -124,6 +124,10 @@ public class RadioSwatchGroupBase<HandlerType: RadioSwatchBase>: SelectorGroupSe
collectionView.reloadData()
}
public func reload() {
collectionView.reloadData()
}
//--------------------------------------------------
// MARK: - UICollectionViewDelegateFlowLayout
//--------------------------------------------------

View File

@ -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()

View File

@ -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
}
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -0,0 +1,19 @@
//
// EnumSubset.swift
// VDS
//
// Created by Matt Bruce on 12/19/22.
//
import Foundation
public protocol EnumSubset<T>: RawRepresentable, CaseIterable {
associatedtype T:RawRepresentable
var defaultValue: T { get }
}
extension EnumSubset where RawValue == T.RawValue {
public var value: T {
T(rawValue: rawValue) ?? defaultValue
}
}

View File

@ -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
}
}