// // SelectorItemBase.swift // VDS // // Created by Matt Bruce on 6/5/23. // import Foundation import UIKit import Combine import VDSTokens /// Base Class used to build out a SelectorControlable control. open class SelectorItemBase: Control, Errorable, Changeable { //-------------------------------------------------- // 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 shouldShowError: Bool { guard showError && isEnabled && errorText?.isEmpty == false else { return false } return true } private var shouldShowLabels: Bool { guard labelText?.isEmpty == false || childText?.isEmpty == false || labelAttributedText?.string.isEmpty == false || childAttributedText?.string.isEmpty == false else { return false } return true } private var mainStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .vertical } private var selectorStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .horizontal } private var selectorLabelStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var onChangeSubscriber: AnyCancellable? /// Label used to render labelText. open var label = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .boldBodyLarge } /// Label used to render childText. open var childLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodyLarge } /// Label used to render errorText. open var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodyMedium } /// Generic Object used to allow the user to 'Select'. open var selectorView = Selector() open override var isSelected: Bool { didSet { setNeedsUpdate() } } /// Text shown in the label. open var labelText: String? { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the labelText. open var labelTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } } /// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var labelAttributedText: NSAttributedString? { didSet { label.attributedText = labelAttributedText setNeedsUpdate() } } /// Text shown in the childLabel. open var childText: String? { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the childText. open var childTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } } /// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var childAttributedText: NSAttributedString? { didSet { childLabel.attributedText = childAttributedText setNeedsUpdate() } } /// Override UIControl state to add the .error state if showError is true. open override var state: UIControl.State { get { var state = super.state if showError { state.insert(.error) } return state } } var _showError: Bool = false open var showError: Bool { get { _showError } set { if !isSelected && _showError != newValue { _showError = newValue setNeedsUpdate() } } } open var errorText: String? { didSet { setNeedsUpdate() } } open var inputId: String? { didSet { setNeedsUpdate() } } open var value: AnyHashable? { hiddenValue } open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() onClick = { control in control.toggle() } } /// 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() isAccessibilityElement = true accessibilityTraits = .button addSubview(mainStackView) mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) selectorStackView.addArrangedSubview(selectorLabelStackView) selectorLabelStackView.addArrangedSubview(label) selectorLabelStackView.addArrangedSubview(childLabel) mainStackView .pinTop() .pinLeading() .pinTrailing() .pinBottom(0, .defaultHigh) } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateLabels() selectorView.showError = showError selectorView.isSelected = isSelected selectorView.isHighlighted = isHighlighted selectorView.isEnabled = isEnabled selectorView.surface = surface } /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false label.reset() childLabel.reset() errorLabel.reset() label.textStyle = .boldBodyLarge childLabel.textStyle = .bodyLarge errorLabel.textStyle = .bodyMedium labelText = nil labelTextAttributes = nil labelAttributedText = nil childText = nil childTextAttributes = nil childAttributedText = nil showError = false errorText = nil inputId = nil isSelected = false onChange = nil shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- /// Update all labels with Text as well as adding and removing the labels. func updateLabels() { //deal with labels if shouldShowLabels { //add the stackview to hold the 2 labels //top label if let labelText { label.surface = surface label.isEnabled = isEnabled label.attributes = labelTextAttributes label.text = labelText label.isHidden = false } else if labelAttributedText != nil { label.isHidden = false } else { label.isHidden = true } //bottom label if let childText { childLabel.text = childText childLabel.surface = surface childLabel.isEnabled = isEnabled childLabel.attributes = childTextAttributes childLabel.isHidden = false } else if childAttributedText != nil { childLabel.isHidden = false } else { childLabel.isHidden = true } selectorStackView.spacing = 12 selectorLabelStackView.spacing = 4 selectorLabelStackView.isHidden = false } else { selectorStackView.spacing = 0 selectorLabelStackView.spacing = 0 selectorLabelStackView.isHidden = true } //either add/remove the error from the main stack if let errorText, shouldShowError { errorLabel.text = errorText errorLabel.surface = surface errorLabel.isEnabled = isEnabled mainStackView.spacing = 8 errorLabel.isHidden = false } else { mainStackView.spacing = 0 errorLabel.isHidden = true } } /// This will change to state of the Selector. open func toggle() {} }