// // SelectorControl.swift // VDS // // Created by Matt Bruce on 8/8/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine open class SelectorBase: Control, Changable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var mainStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.alignment = .top stackView.axis = .vertical return stackView }() private var selectorStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.alignment = .top stackView.axis = .horizontal return stackView }() private var selectorLabelStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical return stackView }() private var primaryLabel: Label = { let label = Label() label.translatesAutoresizingMaskIntoConstraints = false return label }() private var secondaryLabel: Label = { let label = Label() label.translatesAutoresizingMaskIntoConstraints = false return label }() private var errorLabel: Label = { let label = Label() label.translatesAutoresizingMaskIntoConstraints = false return label }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var selectorView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() public var onChange: Blocks.ActionBlock? @Proxy(\.model.id) open var id: UUID //can't bind to @Proxy open override var isSelected: Bool { get { model.selected } set { if model.selected != newValue { model.selected = newValue } } } @Proxy(\.model.labelText) open var labelText: String? @Proxy(\.model.childText) open var childText: String? @Proxy(\.model.hasError) open var hasError: Bool @Proxy(\.model.errorText) open var errorText: String? @Proxy(\.model.inputId) open var inputId: String? @Proxy(\.model.value) open var value: AnyHashable? @Proxy(\.model.dataAnalyticsTrack) open var dataAnalyticsTrack: String? @Proxy(\.model.dataClickStream) open var dataClickStream: String? @Proxy(\.model.dataTrack) open var dataTrack: String? @Proxy(\.model.accessibilityHintEnabled) open var accessibilityHintEnabled: String? @Proxy(\.model.accessibilityHintDisabled) open var accessibilityHintDisabled: String? @Proxy(\.model.accessibilityValueEnabled) open var accessibilityValueEnabled: String? @Proxy(\.model.accessibilityValueDisabled) open var accessibilityValueDisabled: String? @Proxy(\.model.accessibilityLabelEnabled) open var accessibilityLabelEnabled: String? @Proxy(\.model.accessibilityLabelDisabled) open var accessibilityLabelDisabled: String? //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var knobLeadingConstraint: NSLayoutConstraint? private var knobTrailingConstraint: NSLayoutConstraint? private var knobHeightConstraint: NSLayoutConstraint? private var knobWidthConstraint: NSLayoutConstraint? private var selectorHeightConstraint: NSLayoutConstraint? private var selectorWidthConstraint: NSLayoutConstraint? //functions //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(Self.tap))) isAccessibilityElement = true accessibilityTraits = .button addSubview(mainStackView) mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) selectorStackView.addArrangedSubview(selectorLabelStackView) selectorLabelStackView.addArrangedSubview(primaryLabel) selectorLabelStackView.addArrangedSubview(secondaryLabel) let selectorSize = getSelectorSize() selectorHeightConstraint = selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height) selectorHeightConstraint?.isActive = true selectorWidthConstraint = selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width) selectorWidthConstraint?.isActive = true updateSelector(model) mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } func updateLabels(_ viewModel: ModelType) { //deal with labels if viewModel.shouldShowLabels { //add the stackview to hold the 2 labels //top label if let labelModel = viewModel.labelModel { primaryLabel.set(with: labelModel) primaryLabel.isHidden = false } else { primaryLabel.isHidden = true } //bottom label if let childModel = viewModel.childModel { secondaryLabel.set(with: childModel) secondaryLabel.isHidden = false } else { secondaryLabel.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 errorModel = model.errorModel, model.shouldShowError { errorLabel.set(with: errorModel) mainStackView.spacing = 8 errorLabel.isHidden = false } else { mainStackView.spacing = 0 errorLabel.isHidden = true } } public override func reset() { super.reset() updateSelector(model) setAccessibilityLabel() onChange = nil } //-------------------------------------------------- // MARK: - Selector View //-------------------------------------------------- open func getSelectorSize() -> CGSize { fatalError("must override") } open func updateSelector(_ viewModel: ModelType) { } //-------------------------------------------------- // MARK: - Actions //-------------------------------------------------- open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { super.sendAction(action, to: target, for: event) toggleAndAction() } open override func sendActions(for controlEvents: UIControl.Event) { super.sendActions(for: controlEvents) toggleAndAction() } @objc func tap() { sendActions(for: .touchUpInside) } /// This will checkbox the state of the Checkbox and execute the actionBlock if provided. open func toggleAndAction() { //removed error if hasError && isSelected == false { hasError.toggle() } isSelected.toggle() onChange?() } override open func accessibilityActivate() -> Bool { // Hold state in case User wanted isAnimated to remain off. guard isUserInteractionEnabled else { return false } sendActions(for: .touchUpInside) return true } //-------------------------------------------------- // MARK: - UIResponder //-------------------------------------------------- open override func touchesEnded(_ touches: Set, with event: UIEvent?) { sendActions(for: .touchUpInside) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- /// Follow the SwiftUI View paradigm /// - Parameter viewModel: state open override func shouldUpdateView(viewModel: ModelType) -> Bool { let update = viewModel.selected != model.selected || viewModel.labelText != model.labelText || viewModel.childText != model.childText || viewModel.hasError != model.hasError || viewModel.surface != model.surface || viewModel.disabled != model.disabled return update } open override func updateView(viewModel: ModelType) { let enabled = !viewModel.disabled updateLabels(viewModel) updateSelector(viewModel) setAccessibilityHint(enabled) setAccessibilityValue(viewModel.selected) setAccessibilityLabel(viewModel.selected) isUserInteractionEnabled = !viewModel.disabled setNeedsLayout() layoutIfNeeded() } public func selectedPublisher() -> AnyPublisher { self.publisher(for: \.isSelected) .map({ _ in return self.model }) .eraseToAnyPublisher() } }