// // RadioBox.swift // VDS // // Created by Matt Bruce on 8/23/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSRadioBox) public class RadioBox: RadioBoxBase{} @objc(VDSSoloRadioBox) public class SoloRadioBox: RadioBoxBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } @objc(VDSRadioBoxBase) open class RadioBoxBase: Control, BinaryColorable, Accessable, DataTrackable{ //-------------------------------------------------- // 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 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 } }() open var text: String = "Default Text" { didSet { didChange() }} open var textAttributes: [any LabelAttributeModel]? { didSet { didChange() }} open var subText: String? { didSet { didChange() }} open var subTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} open var subTextRight: String? { didSet { didChange() }} open var subTextRightAttributes: [any LabelAttributeModel]? { didSet { didChange() }} open var strikethrough: Bool = false { didSet { didChange() }} open var inputId: String? { didSet { didChange() }} open var value: AnyHashable? { didSet { didChange() }} open var dataAnalyticsTrack: String? { didSet { didChange() }} open var dataClickStream: String? { didSet { didChange() }} open var dataTrack: String? { didSet { didChange() }} open var accessibilityHintEnabled: String? { didSet { didChange() }} open var accessibilityHintDisabled: String? { didSet { didChange() }} open var accessibilityValueEnabled: String? { didSet { didChange() }} open var accessibilityValueDisabled: String? { didSet { didChange() }} open var accessibilityLabelEnabled: String? { didSet { didChange() }} open var accessibilityLabelDisabled: String? { didSet { didChange() }} //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() 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() { //add the stackview to hold the 2 labels //text label textLabel.textPosition = .left textLabel.typograpicalStyle = .BoldBodyLarge textLabel.text = text textLabel.surface = surface textLabel.disabled = disabled textLabel.attributes = textAttributes //subText label if let subText { subTextLabel.textPosition = .left subTextLabel.typograpicalStyle = .BodyLarge subTextLabel.text = subText subTextLabel.surface = surface subTextLabel.disabled = disabled subTextLabel.attributes = subTextAttributes subTextLabel.isHidden = false } else { subTextLabel.isHidden = true } //subTextRight label if let subTextRight { subTextRightLabel.textPosition = .right subTextRightLabel.typograpicalStyle = .BodyLarge subTextRightLabel.text = subTextRight subTextRightLabel.surface = surface subTextRightLabel.disabled = disabled subTextRightLabel.attributes = subTextRightAttributes subTextRightLabel.isHidden = false } else { subTextRightLabel.isHidden = true } } public override func reset() { super.reset() updateSelector() setAccessibilityLabel() } /// This will radioBox the state of the Selector and execute the actionBlock if provided. open func toggle() { //removed error isSelected.toggle() sendActions(for: .valueChanged) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { updateLabels() updateSelector() setAccessibilityHint() setAccessibilityValue(isSelected) setAccessibilityLabel(isSelected) } //-------------------------------------------------- // 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().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().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() { //get the colors let backgroundColor = radioBoxBackgroundColorConfiguration.getColor(self) let borderColor = radioBoxBorderColorConfiguration.getColor(self) let borderWidth = isSelected ? 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(self) shapeLayer?.removeFromSuperlayer() shapeLayer = nil if 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) } } }