// // RadioSwatch.swift // VDS // // Created by Matt Bruce on 8/25/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine public class RadioSwatch: RadioSwatchBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } 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? //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() //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(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 } open func toggle() { isSelected.toggle() sendActions(for: .valueChanged) onChange?() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- /// Follow the SwiftUI View paradigm /// - Parameter viewModel: state open override func shouldUpdateView(viewModel: ModelType) -> Bool { let should = viewModel != model return should } open override func updateView(viewModel: ModelType) { let enabled = !viewModel.disabled updateSelector(viewModel) setAccessibilityHint(enabled) setAccessibilityValue(viewModel.selected) setAccessibilityLabel(viewModel.selected) 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) } } }