// // Checkbox.swift // VDS // // Created by Matt Bruce on 6/5/23. // import Foundation import UIKit import Combine import VDSCoreTokens /// Checkboxes are a multi-select component through which a customer indicates a choice. This is also used within /// ``CheckboxItem`` and ``CheckboxGroup`` @objcMembers @objc(VDSCheckbox) open class Checkbox: SelectorBase { //-------------------------------------------------- // 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 //-------------------------------------------------- /// Whether or not there is animation when the checkbox changes state from non-selected to a selected state. open var isAnimated: Bool = false { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() backgroundColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected) backgroundColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.selected, .highlighted]) backgroundColorConfiguration.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) backgroundColorConfiguration.setSurfaceColors(.clear, .clear, forState: [.error, .disabled]) borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected) borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .highlighted) borderColorConfiguration.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) borderColorConfiguration.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) borderColorConfiguration.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) borderColorConfiguration.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.error, .disabled]) selectorColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .selected) } /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { guard isEnabled else { return } //removed error if showError && isSelected == false { showError.toggle() } isSelected.toggle() sendActions(for: .valueChanged) } open override func layoutSubviews() { super.layoutSubviews() //get the colors let backgroundColor = backgroundColorConfiguration.getColor(self) let borderColor = borderColorConfiguration.getColor(self) let selectorColor = selectorColorConfiguration.getColor(self) if let shapeLayer = shapeLayer, let sublayers = layer.sublayers, sublayers.contains(shapeLayer) { shapeLayer.removeFromSuperlayer() self.shapeLayer = nil } layer.cornerRadius = 2.0 layer.borderWidth = VDSFormControls.borderWidth if shapeLayer == nil { let bounds = bounds let length = max(bounds.size.height, bounds.size.width) guard length > 0.0, shapeLayer == nil else { return } //draw the checkmark layer let xInsetLeft = length * 0.25 let yInsetTop = length * 0.3 let innerWidth = length - (xInsetLeft + length * 0.25) // + Right X Inset let innerHeight = length - (yInsetTop + length * 0.35) // + Bottom Y Inset let startPoint = CGPoint(x: xInsetLeft, y: yInsetTop + (innerHeight / 2)) let pivotOffSet = CGPoint(x: xInsetLeft + (innerWidth * 0.33), y: yInsetTop + innerHeight) let endOffset = CGPoint(x: xInsetLeft + innerWidth, y: yInsetTop) let bezierPath = UIBezierPath() bezierPath.move(to: startPoint) bezierPath.addLine(to: pivotOffSet) bezierPath.addLine(to: endOffset) let shapeLayer = CAShapeLayer() self.shapeLayer = shapeLayer shapeLayer.frame = bounds layer.addSublayer(shapeLayer) shapeLayer.strokeColor = selectorColor.cgColor shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.path = bezierPath.cgPath shapeLayer.lineJoin = .miter shapeLayer.lineWidth = 2 CATransaction.withDisabledAnimations { shapeLayer.strokeEnd = isSelected ? 1 : 0 } } shapeLayer?.removeAllAnimations() if isAnimated && isEnabled && !isHighlighted { let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd") animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear) animateStrokeEnd.duration = 0.3 animateStrokeEnd.fillMode = .both animateStrokeEnd.isRemovedOnCompletion = false animateStrokeEnd.fromValue = !isSelected ? 1 : 0 animateStrokeEnd.toValue = isSelected ? 1 : 0 self.shapeLayer?.add(animateStrokeEnd, forKey: "strokeEnd") UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { self.backgroundColor = backgroundColor self.layer.borderColor = borderColor.cgColor }) } else { CATransaction.withDisabledAnimations { self.shapeLayer?.strokeEnd = isSelected ? 1 : 0 } self.backgroundColor = backgroundColor layer.borderColor = borderColor.cgColor } } } // MARK: AppleGuidelinesTouchable extension Checkbox: AppleGuidelinesTouchable { /// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea. override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { Self.acceptablyOutsideBounds(point: point, bounds: bounds) } }