// // RadioSwatch.swift // VDS // // Created by Matt Bruce on 8/25/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSRadioSwatch) open class RadioSwatch: Control { //-------------------------------------------------- // 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().with { $0.translatesAutoresizingMaskIntoConstraints = false } public var fillView = 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() }} //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(fillView) selectorView.pinToSuperView() let selectorSize = getSelectorSize() selectorView.height(selectorSize.height) selectorView.width(selectorSize.width) fillView.centerXAnchor.constraint(equalTo: selectorView.centerXAnchor).isActive = true fillView.centerYAnchor.constraint(equalTo: selectorView.centerYAnchor).isActive = true fillView.height(fillSize.height) fillView.width(fillSize.width) } open override func reset() { super.reset() fillImage = nil text = "" primaryColor = nil secondaryColor = nil strikethrough = false inputId = nil value = nil setNeedsDisplay() } open func toggle() { isSelected.toggle() sendActions(for: .valueChanged) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { layer.setNeedsDisplay() } public override func updateAccessibilityLabel() { accessibilityLabel = text } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- private var strikeThroughLineThickness: CGFloat = VDSFormControls.widthBorder private var selectorBorderWidth: CGFloat = VDSFormControls.widthBorder public let swatchSize = CGSize(width: 48, height: 48) public let fillSize = CGSize(width: 36, height: 36) public let disabledAlpha = 0.5 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: .highlighted) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected) } //-------------------------------------------------- // 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 drawOuterBorder = isSelected || isHighlighted let backgroundColor = UIColor.clear let borderColor = isSelected ? borderColorConfiguration.getColor(self) : .clear let fillBorderColor = borderColorConfiguration.getColor(self) selectorView.backgroundColor = backgroundColor selectorView.layer.borderColor = borderColor.cgColor selectorView.layer.cornerRadius = selectorView.bounds.width * 0.5 selectorView.layer.borderWidth = drawOuterBorder ? 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 = borderColorConfiguration.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 } }