// // TitleLockup.swift // VDS // // Created by Matt Bruce on 12/19/22. // import Foundation import UIKit import VDSColorTokens import Combine @objc(VDSTitleLockup) open class TitleLockup: View { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum TextPosition: String, EnumSubset { case left, center public var defaultValue: VDS.TextPosition { .left } } //-------------------------------------------------- // 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 standardStyleConfiguration: StandardStyleConfigurationProvider = StandardStyleConfigurationProvider(styleConfigurations: [ .init(deviceType: .iPad, titleStandardStyles: [.titleSmall, .titleMedium], spacingConfigurations: [ .init(otherStandardStyles: [.bodySmall, .bodyMedium, .bodyLarge], topSpacing: VDSLayout.Spacing.space2X.value, bottomSpacing: VDSLayout.Spacing.space2X.value) ]), .init(deviceType: .iPad, titleStandardStyles: [.titleLarge], spacingConfigurations: [ .init(otherStandardStyles: [.bodyLarge, .bodySmall, .bodyMedium, .titleSmall], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space3X.value) ]), .init(deviceType: .iPad, titleStandardStyles: [.titleXLarge], spacingConfigurations: [ .init(otherStandardStyles: [.titleMedium, .bodyLarge], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space4X.value) ]), .init(deviceType: .iPad, titleStandardStyles: [.title2XLarge, .featureXSmall], spacingConfigurations: [ .init(otherStandardStyles: [.bodyLarge], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space6X.value), .init(otherStandardStyles: [.titleMedium, .titleLarge], topSpacing: VDSLayout.Spacing.space4X.value, bottomSpacing: VDSLayout.Spacing.space6X.value), ]), .init(deviceType: .iPad, titleStandardStyles: [.featureSmall, .featureMedium], spacingConfigurations: [ .init(otherStandardStyles: [.titleLarge, .titleMedium], topSpacing: VDSLayout.Spacing.space4X.value, bottomSpacing: VDSLayout.Spacing.space6X.value), .init(otherStandardStyles: [.bodyLarge], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space6X.value), ]), .init(deviceType: .iPhone, titleStandardStyles: [.titleSmall], spacingConfigurations: [ .init(otherStandardStyles: [.bodySmall, .bodyMedium], topSpacing: VDSLayout.Spacing.space2X.value, bottomSpacing: VDSLayout.Spacing.space2X.value) ]), .init(deviceType: .iPhone, titleStandardStyles: [.titleMedium, .titleLarge], spacingConfigurations: [ .init(otherStandardStyles: [.bodySmall, .bodyMedium, .bodyLarge], topSpacing: VDSLayout.Spacing.space2X.value, bottomSpacing: VDSLayout.Spacing.space2X.value) ]), .init(deviceType: .iPhone, titleStandardStyles: [.titleXLarge], spacingConfigurations: [ .init(otherStandardStyles: [.bodyLarge, .bodySmall, .bodyMedium, .titleMedium], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space3X.value) ]), .init(deviceType: .iPhone, titleStandardStyles: [.title2XLarge, .featureXSmall], spacingConfigurations: [ .init(otherStandardStyles: [.bodyLarge, .bodyMedium, .titleMedium], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space4X.value) ]), .init(deviceType: .iPhone, titleStandardStyles: [.featureSmall], spacingConfigurations: [ .init(otherStandardStyles: [.titleLarge, .bodyLarge], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space4X.value) ]), .init(deviceType: .iPhone, titleStandardStyles: [.featureMedium], spacingConfigurations: [ .init(otherStandardStyles: [.titleLarge, .titleXLarge], topSpacing: VDSLayout.Spacing.space4X.value, bottomSpacing: VDSLayout.Spacing.space6X.value), .init(otherStandardStyles: [.bodyLarge], topSpacing: VDSLayout.Spacing.space3X.value, bottomSpacing: VDSLayout.Spacing.space6X.value) ]), ]) //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var textPosition: TextPosition = .left { didSet { setNeedsUpdate() }} //first row open var eyebrowLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var eyebrowModel: EyebrowModel? { didSet { setNeedsUpdate() }} //second row open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var titleModel: TitleModel? { didSet { setNeedsUpdate() }} //third row open var subTitleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } open var subTitleModel: SubTitleModel? { didSet { setNeedsUpdate() }} //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() accessibilityElements = [eyebrowLabel, titleLabel, subTitleLabel] addSubview(stackView) stackView.spacing = 0.0 stackView.addArrangedSubview(eyebrowLabel) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(subTitleLabel) //pin stackview to edges stackView.pinToSuperView() } /// Resets back to this objects default settings. open override func reset() { super.reset() shouldUpdateView = false textPosition = .left eyebrowModel = nil titleModel = nil subTitleModel = nil shouldUpdateView = true setNeedsUpdate() } private var otherStandardStyle: OtherStandardStyle { if let subTitleModel, !subTitleModel.text.isEmpty { return subTitleModel.standardStyle } else if let eyebrowModel, !eyebrowModel.text.isEmpty { return eyebrowModel.standardStyle } else { return .bodyLarge } } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { super.updateView() let allLabelsTextPosition = textPosition.value var eyebrowTextIsEmpty = true var titleTextIsEmpty = true var subTitleTextIsEmpty = true var topSpacing: CGFloat = 0.0 var bottomSpacing: CGFloat = 0.0 //get the spacing based on the title style and other style used for eyebrow and subtitle if let titleModel, let config = standardStyleConfiguration.spacing(for: titleModel.standardStyle, otherStandardStyle: otherStandardStyle) { topSpacing = config.topSpacing bottomSpacing = config.bottomSpacing } if let eyebrowModel, !eyebrowModel.text.isEmpty { eyebrowTextIsEmpty = false eyebrowLabel.textPosition = allLabelsTextPosition eyebrowLabel.textStyle = eyebrowModel.isBold ? otherStandardStyle.value.bold : otherStandardStyle.value.regular 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.textStyle = titleModel.textStyle 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.textStyle = otherStandardStyle.value.regular 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 !eyebrowTextIsEmpty && !titleTextIsEmpty { stackView.spacing = topSpacing } else { stackView.spacing = 0.0 } //if either first 2 rows not empty and subtile not empty, create space else collapse if (!eyebrowTextIsEmpty || !titleTextIsEmpty) && !subTitleTextIsEmpty { stackView.setCustomSpacing(bottomSpacing, after: titleLabel) } else if (!eyebrowTextIsEmpty || !titleTextIsEmpty) && subTitleTextIsEmpty { stackView.setCustomSpacing(0.0, after: titleLabel) } //hide/show eyebrowLabel.isHidden = eyebrowTextIsEmpty titleLabel.isHidden = titleTextIsEmpty subTitleLabel.isHidden = subTitleTextIsEmpty } }