// // RadioSwatch.swift // VDS // // Created by Matt Bruce on 8/25/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSRadioSwatch) public class RadioSwatch: RadioSwatchBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } @objc(VDSRadioSwatchBase) open class RadioSwatchBase: Control, Accessable, DataTrackable, BinaryColorable { //-------------------------------------------------- // 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: - Public Properties //-------------------------------------------------- public var selectorView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() public var fillView: UIImageView = { return UIImageView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.contentMode = .scaleAspectFit } }() open var fillImage: UIImage? { didSet { didChange() }} open var text: String = "" { didSet { didChange() }} open var primaryColor: UIColor? { didSet { didChange() }} open var secondaryColor: UIColor? { 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(fillView) 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() setNeedsDisplay() setAccessibilityLabel() } open func toggle() { isSelected.toggle() sendActions(for: .valueChanged) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { setAccessibilityHint() setAccessibilityValue(isSelected) setAccessibilityLabel(isSelected) setNeedsDisplay() } //-------------------------------------------------- // 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) public let disabledAlpha = 0.5 private var radioSwatchBackgroundColorConfiguration = 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().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().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? private var gradientLayer: CAGradientLayer? open func getSelectorSize() -> CGSize { return swatchSize } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any size changes layer.setNeedsDisplay() } open override func draw(_ layer: CALayer, in ctx: CGContext) { let backgroundColor = radioSwatchBackgroundColorConfiguration.getColor(self) let borderColor = isSelected ? radioSwatchBorderColorConfiguration.getColor(self) : .clear let fillBorderColor = radioSwatchFillBorderColorConfiguration.getColor(self) selectorView.backgroundColor = backgroundColor selectorView.layer.borderColor = borderColor.cgColor selectorView.layer.cornerRadius = selectorView.bounds.width * 0.5 selectorView.layer.borderWidth = isSelected ? selectorBorderWidth : 0 selectorView.layer.masksToBounds = true gradientLayer?.removeFromSuperlayer() gradientLayer = nil var fillColorBackground: UIColor = .clear if let fillImage { fillView.image = disabled ? fillImage.image(alpha: disabledAlpha) : fillImage } else { fillView.image = nil if let primary = primaryColor, let secondary = secondaryColor { let firstColor = disabled ? primary.withAlphaComponent(disabledAlpha) : primary let secondColor = disabled ? secondary.withAlphaComponent(disabledAlpha) : secondary let gradient = CAGradientLayer() gradientLayer = gradient gradient.frame = fillView.bounds gradient.colors = [secondColor.cgColor, secondColor.cgColor, firstColor.cgColor, firstColor.cgColor] gradient.locations = [NSNumber(value: 0.0), NSNumber(value: 0.5), NSNumber(value: 0.5), NSNumber(value: 1.0)] gradient.transform = CATransform3DMakeRotation(135.0 / 180.0 * .pi, 0.0, 0.0, 1.0) fillView.layer.addSublayer(gradient) } else { fillColorBackground = primaryColor ?? .white } } fillView.backgroundColor = disabled ? fillColorBackground.withAlphaComponent(disabledAlpha) : fillColorBackground fillView.layer.borderColor = fillBorderColor.cgColor fillView.layer.cornerRadius = fillView.bounds.width * 0.5 fillView.layer.borderWidth = selectorBorderWidth fillView.layer.masksToBounds = true shapeLayer?.removeFromSuperlayer() shapeLayer = nil if strikethrough { let strikeThroughBorderColor = radioSwatchBorderColorConfiguration.getColor(self) 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 = strikeThroughBorderColor.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) } } } extension UIImage { func image(alpha: CGFloat) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, scale) draw(at: .zero, blendMode: .normal, alpha: alpha) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } }