// // Tilet.swift // VDS // // Created by Matt Bruce on 12/19/22. // import Foundation import Foundation import VDSColorTokens import UIKit import Combine /// Tilelet can be configured with a background image and limited text to /// support quick scanning and engagement. A Tilelet is fully clickable and /// while it can include an arrow CTA, it does not require one in order to /// function. @objc(VDSTilelet) open class Tilelet: TileContainer { //-------------------------------------------------- // 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: - Enums //-------------------------------------------------- /// Enum to represent the Vertical Layout of the Text. public enum TextPosition: String, CaseIterable { case top case bottom } /// Enum to represent the Width of the Text. public enum TextWidth { case value(CGFloat) case percentage(CGFloat) } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill $0.spacing = 5 } private var titleLockupContainerView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } private var badgeContainerView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } private let iconContainerView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public override var onClickSubscriber: AnyCancellable? { didSet { isAccessibilityElement = onClickSubscriber != nil } } /// Title lockup positioned in the contentView. open var titleLockup = TitleLockup().with { $0.standardStyleConfiguration = .init(styleConfigurations: [ .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: .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) ]) ]) } /// Badge positioned in the contentView. open var badge = Badge().with { $0.fillColor = .red } /// Descriptive Icon positioned in the contentView. open var descriptiveIcon = Icon() /// Directional Icon positioned in the contentView. open var directionalIcon = Icon().with { $0.name = .rightArrow } private var _textWidth: TextWidth? /// If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered. open var textWidth: TextWidth? { get { _textWidth } set { if let newValue { switch newValue { case .percentage(let percentage): if percentage >= 10 && percentage <= 100.0 { _textWidth = newValue } case .value(let value): if value > 44.0 { _textWidth = newValue } } } else { _textWidth = nil } setNeedsUpdate() } } /// Determines where the text aligns vertically. open var textPostion: TextPosition = .top { didSet { setNeedsUpdate() } } /// If set, this is used to render the badge. open var badgeModel: BadgeModel? { didSet { setNeedsUpdate() } } /// If set, this is used to render the titleLabel of the TitleLockup. open var titleModel: TitleModel? { didSet { setNeedsUpdate() } } /// If set, this is used to render the subTitleLabel of the TitleLockup. open var subTitleModel: SubTitleModel? { didSet { setNeedsUpdate() } } //only 1 Icon can be active private var _descriptiveIconModel: DescriptiveIcon? /// If set, this is used to render the descriptive icon. open var descriptiveIconModel: DescriptiveIcon? { get { _descriptiveIconModel } set { _descriptiveIconModel = newValue; _directionalIconModel = nil setNeedsUpdate() } } private var _directionalIconModel: DirectionalIcon? /// If set, this is used to render the directional icon. open var directionalIconModel: DirectionalIcon? { get { _directionalIconModel } set { _directionalIconModel = newValue; _descriptiveIconModel = nil setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var titleLockupWidthConstraint: NSLayoutConstraint? internal var titleLockupTrailingConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() aspectRatio = .none color = .black addContentView(stackView) accessibilityTraits = .link accessibilityElements = [badge, titleLockup, descriptiveIcon, directionalIcon] //badge badgeContainerView.addSubview(badge) badge .pinTop() .pinLeading() .pinBottom() badge.trailingAnchor.constraint(lessThanOrEqualTo: badgeContainerView.trailingAnchor).isActive = true titleLockupContainerView.addSubview(titleLockup) titleLockup .pinTop() .pinLeading() .pinBottom() titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor) titleLockupTrailingConstraint?.isActive = true iconContainerView.addSubview(descriptiveIcon) iconContainerView.addSubview(directionalIcon) descriptiveIcon .pinLeading() .pinTop() .pinBottom() directionalIcon .pinTrailing() .pinTop() .pinBottom() } /// Resets to default settings. open override func reset() { shouldUpdateView = false aspectRatio = .none color = .black //models badgeModel = nil titleModel = nil subTitleModel = nil descriptiveIconModel = nil directionalIconModel = nil shouldUpdateView = true setNeedsUpdate() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateBadge() updateTitleLockup() updateIcons() layoutIfNeeded() } /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() setAccessibilityLabel(for: [badge.label, titleLockup.eyebrowLabel, titleLockup.titleLabel, titleLockup.subTitleLabel]) } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func updateBadge() { if let badgeModel { badge.text = badgeModel.text badge.fillColor = badgeModel.fillColor badge.numberOfLines = badgeModel.numberOfLines badge.surface = badgeModel.surface badge.maxWidth = badgeModel.maxWidth if badgeContainerView.superview == nil { stackView.insertArrangedSubview(badgeContainerView, at: 0) setNeedsLayout() } } else { removeFromSuperview(badgeContainerView) } } private func updateTitleLockup() { var showTitleLockup = false if let titleModel, !titleModel.text.isEmpty { showTitleLockup = true } if let subTitleModel, !subTitleModel.text.isEmpty { showTitleLockup = true } if showTitleLockup { //flip the surface for the titleLockup titleLockup.surface = color == .black ? Surface.dark : Surface.light //titleLockup if let textWidth { titleLockupTrailingConstraint?.isActive = false titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(lessThanOrEqualTo: titleLockupContainerView.trailingAnchor) titleLockupTrailingConstraint?.isActive = true titleLockupWidthConstraint?.isActive = false switch textWidth { case .value(let value): titleLockupWidthConstraint = titleLockup.widthAnchor.constraint(equalToConstant: value) case .percentage(let percentage): titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: percentage / 100, constant: 0.0) } titleLockupWidthConstraint?.isActive = true } else { titleLockupTrailingConstraint?.isActive = false titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor) titleLockupTrailingConstraint?.isActive = true titleLockupWidthConstraint?.isActive = false } //set models titleLockup.titleModel = titleModel?.toTitleLockupTitleModel() titleLockup.subTitleModel = subTitleModel?.toTitleLockupSubTitleModel() if titleLockupContainerView.superview == nil { stackView.insertArrangedSubview(titleLockupContainerView, at: badgeContainerView.superview == nil ? 0 : 1) setNeedsLayout() } } else { removeFromSuperview(titleLockupContainerView) } } private func updateIcons() { //icons var showIconContainerView = false if let descriptiveIconModel { descriptiveIcon.name = descriptiveIconModel.name descriptiveIcon.size = descriptiveIconModel.size descriptiveIcon.surface = descriptiveIconModel.surface showIconContainerView = true } if let directionalIconModel { directionalIcon.size = directionalIconModel.size directionalIcon.surface = directionalIconModel.surface showIconContainerView = true } //iconContainer descriptiveIcon.isHidden = descriptiveIconModel == nil directionalIcon.isHidden = directionalIconModel == nil if showIconContainerView { //spacing before iconContainerView var view: UIView? if badgeContainerView.superview != nil { view = badgeContainerView } if titleLockupContainerView.superview != nil { view = titleLockupContainerView } if let view { stackView.setCustomSpacing(padding.tiletSpacing, after: view) } if iconContainerView.superview == nil { stackView.addArrangedSubview(iconContainerView) setNeedsDisplay() } } else { removeFromSuperview(iconContainerView) } } } extension TileContainer.Padding { fileprivate var tiletSpacing: CGFloat { switch self { case .padding2X: return 16 case .padding4X: return 24 case .padding6X: return 32 case .padding8X: return 48 default: return 16 } } }