// // Tab.swift // VDS // // Created by Matt Bruce on 5/18/23. // import Foundation import UIKit import VDSCoreTokens import Combine extension Tabs { @objc(VDSTab) open class Tab: Control, Groupable { //-------------------------------------------------- // 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 //-------------------------------------------------- /// TextStyle used on the label. private var textStyle: TextStyle { if size == .medium { return .boldBodyLarge } else { //specs show that the font size shouldn't change however boldTitleSmall does //change point size between iPad/iPhone. This is a "fix" so each device will //load the correct pointSize return UIDevice.isIPad ? .boldTitleSmall : .boldTitleMedium } } private var labelWidthConstraint: NSLayoutConstraint? private var labelLeadingConstraint: NSLayoutConstraint? private var labelTopConstraint: NSLayoutConstraint? private var labelBottomConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- ///position of the tab open var index: Int = 0 ///label to write out the text open var label: Label = Label().with { $0.isUserInteractionEnabled = false $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.setContentHuggingPriority(.required, for: .horizontal) } ///orientation of the tabs open var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } } ///Size for tab open var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } } ///Number of lines in the Label. open var numberOfLines: Int = 0 { didSet { setNeedsUpdate() } } ///Text position left or center open var textAlignment: TextAlignment = .left { didSet { setNeedsUpdate() } } ///Sets the Position of the Selected/Hover Border Accent for All Tabs. open var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } } ///If provided, it will set fixed width for this Tab. open var width: CGFloat? { didSet { setNeedsUpdate() } } ///The text label of the tab. open var text: String = "Tab" { didSet { setNeedsUpdate() } } ///Minimum width for the tab open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } } open override var shouldHighlight: Bool { false } //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- private var textColorConfiguration: SurfaceColorConfiguration { isSelected ? textColorSelectedConfiguration : textColorNonSelectedConfiguration } private var textColorNonSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOndark) private var textColorSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) private var indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteRed, VDSColor.elementsPrimaryOndark) private var indicatorWidth: CGFloat = 4.0 private var indicatorSide: UIRectEdge { guard orientation == .horizontal else { return .left } return indicatorPosition.value } private var leadingSpace: CGFloat { guard orientation == .vertical else { return 0 } return VDSLayout.space4X } private var otherSpace: CGFloat { if orientation == .horizontal { return size == .medium ? VDSLayout.space3X : VDSLayout.space4X } else { return VDSLayout.space2X } } private let layoutGuide = UILayoutGuide() private func updateWidth() { labelWidthConstraint?.isActive = false guard let width, width > minWidth else { return } labelWidthConstraint?.constant = width labelWidthConstraint?.isActive = true } //-------------------------------------------------- // 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() addLayoutGuide(layoutGuide) addSubview(label) accessibilityTraits = .button isAccessibilityElement = true //pin layoutguide layoutGuide.pinToSuperView() //pin trailing label.pinTrailing(layoutGuide.trailingAnchor) //setup constraints labelWidthConstraint = layoutGuide.width(constant: 0).with { $0.isActive = false } layoutGuide.widthGreaterThanEqualTo(minWidth) labelTopConstraint = label.pinTop(anchor: layoutGuide.topAnchor) labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor) labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh) bridge_accessibilityLabelBlock = { [weak self] in guard let self else { return "" } return text } } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() guard !text.isEmpty else { return } accessibilityIdentifier = "VDSTab:\(text)" //constaints updateWidth() labelLeadingConstraint?.constant = leadingSpace labelTopConstraint?.constant = otherSpace labelBottomConstraint?.constant = -otherSpace //label properties label.textStyle = textStyle label.text = text label.surface = surface label.textAlignment = textAlignment.value label.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() setNeedsLayout() } open override func layoutSubviews() { super.layoutSubviews() removeBorders() if isSelected { addBorder(side: indicatorSide, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) } } } }