// // 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. //-------------------------------------------------- // 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) } } internal 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) }() internal 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) }() 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) } }