// // 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 topTextStyleSpacingConfig: TextStyle.SpacingConfig = { let configs = [ TextStyle.DeviceSpacingConfig([.boldTitleLarge, .titleLarge], neighboring: [.bodySmall, .bodyMedium, .bodyLarge], spacing: 12.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitleXLarge, .titleXLarge], neighboring: [.titleMedium, .bodyLarge], spacing: 12.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.titleMedium, .titleLarge], spacing: 16.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.bodyLarge], spacing: 12.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall, .boldFeatureMedium, .featureMedium], neighboring: [.titleMedium, .titleLarge], spacing: 16.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall, .boldFeatureMedium, .featureMedium], neighboring: [.bodyLarge], spacing: 12.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitleXLarge, .titleXLarge], neighboring: [.bodyLarge, .bodyMedium, .bodySmall, .titleMedium], spacing: 12.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.bodyLarge, .bodyMedium, .titleMedium], spacing: 12.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall], neighboring: [.titleLarge, .bodyLarge], spacing: 12.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureMedium, .featureMedium], neighboring: [.titleLarge, .titleXLarge], spacing: 16.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureMedium, .featureMedium], neighboring: [.bodyLarge], spacing: 12.0, deviceType: .iPhone) ] return TextStyle.SpacingConfig(configs: configs) }() open var bottomTextStyleSpacingConfig: TextStyle.SpacingConfig = { let configs = [ TextStyle.DeviceSpacingConfig([.boldTitleLarge, .titleLarge], neighboring: [.bodySmall, .bodyMedium, .bodyLarge], spacing: 12.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitleXLarge, .titleXLarge], neighboring: [.titleMedium, .bodyLarge], spacing: 16.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.titleMedium, .titleLarge], spacing: 24.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.bodyLarge], spacing: 24.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall, .boldFeatureMedium, .featureMedium], neighboring: [.titleMedium, .titleLarge], spacing: 24.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall, .boldFeatureMedium, .featureMedium], neighboring: [.bodyLarge], spacing: 24.0, deviceType: .iPad), TextStyle.DeviceSpacingConfig([.boldTitleXLarge, .titleXLarge], neighboring: [.bodyLarge, .bodyMedium, .bodySmall, .titleMedium], spacing: 12.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldTitle2XLarge, .title2XLarge, .boldFeatureXSmall, .featureXSmall], neighboring: [.bodyLarge, .bodyMedium, .titleMedium], spacing: 16, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureSmall, .featureSmall], neighboring: [.titleLarge, .bodyLarge], spacing: 16.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureMedium, .featureMedium], neighboring: [.titleLarge, .titleXLarge], spacing: 24.0, deviceType: .iPhone), TextStyle.DeviceSpacingConfig([.boldFeatureMedium, .featureMedium], neighboring: [.bodyLarge], spacing: 24.0, deviceType: .iPhone) ] return TextStyle.SpacingConfig(configs: configs) }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var textPosition: TextPosition = .left { didSet { setNeedsUpdate() }} //style open var otherTextStyle: OtherTextStyle = UIDevice.isIPad ? .bodyLarge : .bodyMedium { 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() } open override func reset() { super.reset() shouldUpdateView = false textPosition = .left eyebrowModel = nil titleModel = nil subTitleModel = nil otherTextStyle = UIDevice.isIPad ? .bodyLarge : .bodyMedium shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { super.updateView() let allLabelsTextPosition = textPosition.value var eyebrowTextIsEmpty = true var titleTextIsEmpty = true var subTitleTextIsEmpty = true if let eyebrowModel, !eyebrowModel.text.isEmpty { eyebrowTextIsEmpty = false eyebrowLabel.textPosition = allLabelsTextPosition eyebrowLabel.textStyle = otherTextStyle.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.textStyle = titleModel.textStyle.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.textStyle = otherTextStyle.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 = topTextStyleSpacingConfig.spacing(for: titleModel.textStyle.value, neighboring: otherTextStyle.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 = bottomTextStyleSpacingConfig.spacing(for: titleModel.textStyle.value, neighboring: otherTextStyle.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 } }