From ba4bd4e1f5a1bfee99d24f83f3054e5530fbbfdc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 30 Aug 2022 09:18:09 -0500 Subject: [PATCH] first cut of radioswatch Signed-off-by: Matt Bruce --- VDS/Components/RadioSwatch/RadioSwatch.swift | 286 +++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/VDS/Components/RadioSwatch/RadioSwatch.swift b/VDS/Components/RadioSwatch/RadioSwatch.swift index 22be4f21..fcb9e2cc 100644 --- a/VDS/Components/RadioSwatch/RadioSwatch.swift +++ b/VDS/Components/RadioSwatch/RadioSwatch.swift @@ -6,3 +6,289 @@ // import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +public class RadioSwatch: RadioSwatchBase{} + +open class RadioSwatchBase: Control, Changable { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var selectorView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + public var fillView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + 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.text) + open var text: 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() + addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(Self.tap))) + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(selectorView) + + selectorView.addSubview(fillView) + + 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 + + let selectorSize = getSelectorSize() + selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height).isActive = true + selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width).isActive = true + + fillView.centerXAnchor.constraint(equalTo: selectorView.centerXAnchor).isActive = true + fillView.centerYAnchor.constraint(equalTo: selectorView.centerYAnchor).isActive = true + + fillView.heightAnchor.constraint(equalToConstant: fillSize.height).isActive = true + fillView.widthAnchor.constraint(equalToConstant: fillSize.width).isActive = true + + } + + public override func reset() { + super.reset() + updateSelector(model) + setAccessibilityLabel() + onChange = nil + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + open override func sendActions(for controlEvents: UIControl.Event) { + super.sendActions(for: controlEvents) + if controlEvents.contains(.touchUpInside) { + toggle() + } + } + + @objc func tap() { + sendActions(for: .touchUpInside) + + } + + /// This will radioBox the state of the Selector and execute the actionBlock if provided. + open func toggle() { + isSelected.toggle() + sendActions(for: .valueChanged) + 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: - 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.primaryColor != model.primaryColor + || viewModel.secondaryColor != model.secondaryColor + || viewModel.surface != model.surface + || viewModel.disabled != model.disabled + return update + } + + open override func updateView(viewModel: ModelType) { + let enabled = !viewModel.disabled + + updateSelector(viewModel) + setAccessibilityHint(enabled) + setAccessibilityValue(viewModel.selected) + setAccessibilityLabel(viewModel.selected) + isUserInteractionEnabled = !viewModel.disabled + setNeedsLayout() + layoutIfNeeded() + } + + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + private var strikeThroughLineThickness: CGFloat = 1.0 + private var selectorBorderWidth: CGFloat = 1.0 + public let swatchSize = CGSize(width: 48, height: 48) + public let fillSize = CGSize(width: 36, height: 36) + + private var radioSwatchBackgroundColorConfiguration: 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 radioSwatchBorderColorConfiguration: 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 + } + }() + + private var radioSwatchFillBorderColorConfiguration: DisabledSurfaceColorConfiguration = { + return DisabledSurfaceColorConfiguration().with { + $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.enabled.darkColor = VDSColor.elementsPrimaryOndark + $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.disabled.darkColor = VDSColor.interactiveDisabledOndark + } + }() + //-------------------------------------------------- + // MARK: - RadioBox View Updates + //-------------------------------------------------- + /// Manages the appearance of the radioSwatch. + + private var shapeLayer: CAShapeLayer? + + open func getSelectorSize() -> CGSize { + return swatchSize + } + + open func updateSelector(_ viewModel: ModelType) { + //get the colors + let backgroundColor = radioSwatchBackgroundColorConfiguration.getColor(viewModel) + let borderColor = viewModel.selected ? radioSwatchBorderColorConfiguration.getColor(viewModel) : .clear + let fillBorderColor = radioSwatchFillBorderColorConfiguration.getColor(viewModel) + + selectorView.backgroundColor = backgroundColor + selectorView.layer.borderColor = borderColor.cgColor + selectorView.layer.cornerRadius = selectorView.bounds.width * 0.5 + selectorView.layer.borderWidth = viewModel.selected ? selectorBorderWidth : 0 + selectorView.layer.masksToBounds = true + + var fillColor: UIColor = viewModel.primaryColor ?? .white + fillView.backgroundColor = fillColor + fillView.layer.borderColor = fillBorderColor.cgColor + fillView.layer.cornerRadius = fillView.bounds.width * 0.5 + fillView.layer.borderWidth = selectorBorderWidth + + 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 = radioSwatchBorderColorConfiguration.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 strikeThrough = CAShapeLayer() + strikeThrough.name = "strikethrough" + strikeThrough.fillColor = nil + strikeThrough.opacity = 1.0 + strikeThrough.lineWidth = strikeThroughLineThickness + strikeThrough.strokeColor = borderColor.cgColor + + let linePath = UIBezierPath() + linePath.move(to: CGPoint(x: 0, y: bounds.height)) + linePath.addLine(to: CGPoint(x: bounds.width, y: 0)) + linePath.addClip() + strikeThrough.path = linePath.cgPath + shapeLayer = strikeThrough + selectorView.layer.addSublayer(strikeThrough) + } + } +}