// // 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 } } } @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 otherTypograpicalStyle: TitleLockupOtherTypographicalStyle = UIDevice.isIPad ? .BodyLarge : .BodyMedium { didSet { didChange() }} //first row open var eyebrowLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var eyebrowModel: TitleLockupEyebrowModel? { didSet { didChange() }} //second row open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var titleModel: TitleLockupTitleModel? { didSet { didChange() }} //third row open var subTitleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var subTitleModel: TitleLockupSubTitleModel? { 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 eyebrowModel = nil titleModel = nil subTitleModel = nil otherTypograpicalStyle = .BodyLarge } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { super.updateView() let allLabelsTextPosition = textPosition.labelTextPosition var eyebrowTextIsEmpty = true var titleTextIsEmpty = true var subTitleTextIsEmpty = true if let eyebrowModel, !eyebrowModel.text.isEmpty { eyebrowTextIsEmpty = false eyebrowLabel.textPosition = allLabelsTextPosition eyebrowLabel.typograpicalStyle = otherTypograpicalStyle.value eyebrowLabel.text = eyebrowModel.text eyebrowLabel.attributes = eyebrowModel.textAttributes eyebrowLabel.numberOfLines = eyebrowModel.numberOfLines eyebrowLabel.surface = surface } else { eyebrowLabel.reset() } if let titleModel, !titleModel.text.isEmpty { titleTextIsEmpty = false titleLabel.textPosition = allLabelsTextPosition titleLabel.typograpicalStyle = titleModel.typographicalStyle.value titleLabel.text = titleModel.text titleLabel.attributes = titleModel.textAttributes titleLabel.numberOfLines = titleModel.numberOfLines titleLabel.surface = surface } else { titleLabel.reset() } if let subTitleModel, !subTitleModel.text.isEmpty { subTitleTextIsEmpty = false subTitleLabel.textPosition = allLabelsTextPosition subTitleLabel.typograpicalStyle = otherTypograpicalStyle.value subTitleLabel.text = subTitleModel.text subTitleLabel.attributes = subTitleModel.textAttributes subTitleLabel.numberOfLines = subTitleModel.numberOfLines subTitleLabel.surface = surface subTitleLabel.disabled = subTitleModel.textColor == .secondary } else { subTitleLabel.reset() } //if both first 2 rows not empty set spacing if let eyebrowModel, let titleModel, !eyebrowModel.text.isEmpty, !titleModel.text.isEmpty { stackView.spacing = topTypographicalStyleSpacingConfig.spacing(for: titleModel.typographicalStyle.value, neighboring: otherTypograpicalStyle.value) } else { stackView.spacing = 0.0 } //if either first 2 rows not empty and subtile not empty, create space else collapse if let titleModel, (!eyebrowTextIsEmpty || !titleTextIsEmpty) && !subTitleTextIsEmpty { let bottomSpace = bottomTypographicalStyleSpacingConfig.spacing(for: titleModel.typographicalStyle.value, neighboring: otherTypograpicalStyle.value) stackView.setCustomSpacing(bottomSpace, after: titleLabel) } else if (!eyebrowTextIsEmpty || !titleTextIsEmpty) && subTitleTextIsEmpty { stackView.setCustomSpacing(0.0, after: titleLabel) } //hide/show eyebrowLabel.isHidden = eyebrowTextIsEmpty titleLabel.isHidden = titleTextIsEmpty subTitleLabel.isHidden = subTitleTextIsEmpty } } extension TypographicalStyle { func isWithin(_ collection: [TypographicalStyle]) -> Bool { (collection.first(where: {$0 == self}) != nil) } }