// // RadioBox.swift // VDS // // Created by Matt Bruce on 8/23/22. // import Foundation import UIKit import Combine import VDSColorTokens import VDSFormControlsTokens @objc(VDSRadioBox) open class RadioBox: Control, 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 mainStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .vertical $0.spacing = 0 } private var selectorStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .horizontal $0.spacing = 12 } private var selectorLeftLabelStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.spacing = 4 $0.isHidden = false } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var onChangeSubscriber: AnyCancellable? { willSet { if let onChangeSubscriber { onChangeSubscriber.cancel() } } } open var textLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.textStyle = .boldBodyLarge } open var subTextLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.textStyle = .bodyLarge } open var subTextRightLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .right $0.textStyle = .bodyLarge } public var selectorView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } open var text: String = "Default Text" { didSet { setNeedsUpdate() }} open var textAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} open var textAttributedText: NSAttributedString? { didSet { textLabel.useAttributedText = !(textAttributedText?.string.isEmpty ?? true) textLabel.attributedText = textAttributedText setNeedsUpdate() } } open var subText: String? { didSet { setNeedsUpdate() }} open var subTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} open var subTextAttributedText: NSAttributedString? { didSet { subTextLabel.useAttributedText = !(subTextAttributedText?.string.isEmpty ?? true) subTextLabel.attributedText = subTextAttributedText setNeedsUpdate() } } open var subTextRight: String? { didSet { setNeedsUpdate() }} open var subTextRightAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} open var subTextRightAttributedText: NSAttributedString? { didSet { subTextRightLabel.useAttributedText = !(subTextRightAttributedText?.string.isEmpty ?? true) subTextRightLabel.attributedText = subTextRightAttributedText setNeedsUpdate() } } open var strikethrough: Bool = false { didSet { setNeedsUpdate() }} open var inputId: String? { didSet { setNeedsUpdate() }} open var value: AnyHashable? { didSet { setNeedsUpdate() }} //functions //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func initialSetup() { super.initialSetup() onClick = { control in control.toggle() } } open override func setup() { super.setup() isAccessibilityElement = true accessibilityTraits = .button addSubview(selectorView) selectorView.isUserInteractionEnabled = false selectorView.addSubview(mainStackView) mainStackView.addArrangedSubview(selectorStackView) selectorStackView.addArrangedSubview(selectorLeftLabelStackView) selectorStackView.addArrangedSubview(subTextRightLabel) selectorLeftLabelStackView.addArrangedSubview(textLabel) selectorLeftLabelStackView.addArrangedSubview(subTextLabel) updateSelector() selectorView.pinToSuperView() mainStackView.pinToSuperView(.init(top: 16, left: 16, bottom: 16, right: 16)) } func updateLabels() { //add the stackview to hold the 2 labels //text label textLabel.text = text textLabel.surface = surface textLabel.disabled = disabled textLabel.attributes = textAttributes //subText label if let subText { subTextLabel.text = subText subTextLabel.surface = surface subTextLabel.disabled = disabled subTextLabel.attributes = subTextAttributes subTextLabel.isHidden = false } else if subTextAttributedText != nil { subTextLabel.isHidden = false } else { subTextLabel.isHidden = true } //subTextRight label if let subTextRight { subTextRightLabel.text = subTextRight subTextRightLabel.surface = surface subTextRightLabel.disabled = disabled subTextRightLabel.attributes = subTextRightAttributes subTextRightLabel.isHidden = false } else if subTextAttributedText != nil { subTextRightLabel.isHidden = false } else { subTextRightLabel.isHidden = true } } open override func reset() { super.reset() shouldUpdateView = false textLabel.reset() subTextLabel.reset() subTextRightLabel.reset() textLabel.textStyle = .boldBodyLarge subTextLabel.textStyle = .bodyLarge subTextRightLabel.textStyle = .bodyLarge text = "Default Text" textAttributes = nil textAttributedText = nil subText = nil subTextAttributes = nil subTextAttributedText = nil subTextRight = nil subTextRightAttributes = nil subTextRightAttributedText = nil strikethrough = false inputId = nil value = nil isSelected = false shouldUpdateView = true setNeedsUpdate() } /// 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() updateAccessibilityLabel() setNeedsDisplay() } open override func updateAccessibilityLabel() { setAccessibilityLabel(for: [textLabel, subTextLabel, subTextRightLabel]) } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- private var strikeThroughLineThickness: CGFloat = VDSFormControls.widthBorder private var selectorCornerRadius: CGFloat = VDSFormControls.borderradius private var selectorBorderWidthSelected: CGFloat = VDSFormControls.widthBorder + VDSFormControls.widthBorder private var selectorBorderWidth: CGFloat = VDSFormControls.widthBorder private var backgroundColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .selected) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .highlighted) } private var borderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .highlighted) } //-------------------------------------------------- // MARK: - RadioBox View Updates //-------------------------------------------------- /// Manages the appearance of the radioBox. private var shapeLayer: CAShapeLayer? open func updateSelector() { //get the colors let backgroundColor = backgroundColorConfiguration.getColor(self) let borderColor = borderColorConfiguration.getColor(self) let borderWidth = isSelected ? selectorBorderWidthSelected : selectorBorderWidth selectorView.backgroundColor = backgroundColor selectorView.layer.borderColor = borderColor.cgColor selectorView.layer.cornerRadius = selectorCornerRadius selectorView.layer.borderWidth = borderWidth layer.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 = borderColorConfiguration.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) } } }