// // RadioBox.swift // VDS // // Created by Matt Bruce on 8/23/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine public class RadioBox: RadioBoxBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } open class RadioBoxBase: Control, Changable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var mainStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .vertical $0.spacing = 0 } }() private var selectorStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .horizontal } }() private var selectorLeftLabelStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical } }() private var textLabel = Label() private var subTextLabel = Label() private var subTextRightLabel = Label() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var selectorView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() public var onChange: Blocks.ActionBlock? //can't bind to @Proxy open override var isSelected: Bool { get { model.selected } set { if model.selected != newValue { model.selected = newValue } } } @Proxy(\.model.text) open var text: String @Proxy(\.model.subText) open var subText: String? @Proxy(\.model.subTextRight) open var subTextRight: String? @Proxy(\.model.strikethrough) open var strikethrough: Bool @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? //functions //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() //add tapGesture to self publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in self?.sendActions(for: .touchUpInside) }.store(in: &subscribers) isAccessibilityElement = true accessibilityTraits = .button addSubview(selectorView) selectorView.addSubview(mainStackView) mainStackView.addArrangedSubview(selectorStackView) selectorStackView.addArrangedSubview(selectorLeftLabelStackView) selectorStackView.addArrangedSubview(subTextRightLabel) selectorLeftLabelStackView.addArrangedSubview(textLabel) selectorLeftLabelStackView.addArrangedSubview(subTextLabel) selectorStackView.spacing = 12 selectorLeftLabelStackView.spacing = 4 selectorLeftLabelStackView.isHidden = false updateSelector(model) selectorView.topAnchor.constraint(equalTo: topAnchor).isActive = true selectorView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true selectorView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true selectorView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true mainStackView.topAnchor.constraint(equalTo: selectorView.topAnchor, constant: 16).isActive = true mainStackView.leadingAnchor.constraint(equalTo: selectorView.leadingAnchor, constant: 16).isActive = true mainStackView.trailingAnchor.constraint(equalTo: selectorView.trailingAnchor, constant: -16).isActive = true mainStackView.bottomAnchor.constraint(equalTo: selectorView.bottomAnchor, constant: -16).isActive = true } func updateLabels(_ viewModel: ModelType) { //add the stackview to hold the 2 labels //text label textLabel.set(with: viewModel.textModel) //subText label if let subTextModel = viewModel.subTextModel { subTextLabel.set(with: subTextModel) subTextLabel.isHidden = false } else { subTextLabel.isHidden = true } //subTextRight label if let subTextRightModel = viewModel.subTextRightModel { subTextRightLabel.set(with: subTextRightModel) subTextRightLabel.isHidden = false } else { subTextRightLabel.isHidden = true } } public override func reset() { super.reset() updateSelector(model) setAccessibilityLabel() onChange = nil } /// This will radioBox the state of the Selector and execute the actionBlock if provided. open func toggle() { //removed error isSelected.toggle() sendActions(for: .valueChanged) onChange?() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- /// Follow the SwiftUI View paradigm /// - Parameter viewModel: state open override func shouldUpdateView(viewModel: ModelType) -> Bool { let update = viewModel.selected != model.selected || viewModel.text != model.text || viewModel.subText != model.subText || viewModel.subTextRight != model.subTextRight || 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) setNeedsLayout() layoutIfNeeded() } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- private var strikeThroughLineThickness: CGFloat = 1.0 private var selectorCornerRadius: CGFloat = 4.0 private var selectorBorderWidthSelected: CGFloat = 2.0 private var selectorBorderWidth: CGFloat = 1.0 private var radioBoxBackgroundColorConfiguration: BinaryDisabledSurfaceColorConfiguration = { return BinaryDisabledSurfaceColorConfiguration().with { $0.forFalse.enabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.forFalse.enabled.darkColor = VDSFormControlsColor.backgroundOndark $0.forFalse.disabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.forFalse.disabled.darkColor = VDSFormControlsColor.backgroundOndark $0.forTrue.enabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.forTrue.enabled.darkColor = VDSFormControlsColor.backgroundOndark $0.forTrue.disabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.forTrue.disabled.darkColor = VDSFormControlsColor.backgroundOndark } }() private var radioBoxBorderColorConfiguration: BinaryDisabledSurfaceColorConfiguration = { return BinaryDisabledSurfaceColorConfiguration().with { $0.forTrue.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.forTrue.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.forFalse.enabled.lightColor = VDSFormControlsColor.borderOnlight $0.forFalse.enabled.darkColor = VDSFormControlsColor.borderOndark $0.forTrue.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.forTrue.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.forFalse.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.forFalse.disabled.darkColor = VDSColor.interactiveDisabledOndark } }() //-------------------------------------------------- // MARK: - RadioBox View Updates //-------------------------------------------------- /// Manages the appearance of the radioBox. private var shapeLayer: CAShapeLayer? open func updateSelector(_ viewModel: ModelType) { //get the colors let backgroundColor = radioBoxBackgroundColorConfiguration.getColor(viewModel) let borderColor = radioBoxBorderColorConfiguration.getColor(viewModel) let borderWidth = viewModel.selected ? selectorBorderWidthSelected : selectorBorderWidth selectorView.backgroundColor = backgroundColor selectorView.layer.borderColor = borderColor.cgColor selectorView.layer.cornerRadius = selectorCornerRadius selectorView.layer.borderWidth = borderWidth setNeedsDisplay() } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any size changes layer.setNeedsDisplay() } open override func draw(_ layer: CALayer, in ctx: CGContext) { let borderColor = radioBoxBorderColorConfiguration.getColor(model) shapeLayer?.removeFromSuperlayer() shapeLayer = nil if model.strikethrough { let bounds = selectorView.bounds let length = max(bounds.size.height, bounds.size.width) guard length > 0.0, shapeLayer == nil else { return } let border = CAShapeLayer() border.name = "strikethrough" border.fillColor = nil border.opacity = 1.0 border.lineWidth = strikeThroughLineThickness border.strokeColor = borderColor.cgColor let linePath = UIBezierPath() let offsetPercent: CGFloat = 0.005 linePath.move(to: CGPoint(x: selectorCornerRadius, y: bounds.height * (1 - offsetPercent))) linePath.addLine(to: CGPoint(x: bounds.width - selectorCornerRadius, y: bounds.height * offsetPercent)) border.path = linePath.cgPath shapeLayer = border layer.addSublayer(border) } } }