// // Checkbox.swift // VDS // // Created by Matt Bruce on 6/5/23. // import Foundation import UIKit import Combine import VDSColorTokens import VDSFormControlsTokens public protocol SelectorControlable: Control, Changeable { var showError: Bool { get set } var size: CGSize { get set } var backgroundColorConfig: ControlColorConfiguration { get set } var borderColorConfig: ControlColorConfiguration { get set } var selectorColorConfig: ControlColorConfiguration { get set } } @objc(VDSCheckbox) open class Checkbox: Control, SelectorControlable { public var onChangeSubscriber: AnyCancellable? open var isAnimated: Bool = true { didSet { setNeedsUpdate() }} open var size = CGSize(width: 20, height: 20) { didSet { setNeedsUpdate() }} var _showError: Bool = false open var showError: Bool { get { _showError } set { if !isSelected && _showError != newValue { _showError = newValue setNeedsUpdate() } } } open override var state: UIControl.State { get { var state = super.state if showError { state.insert(.error) } return state } } open var backgroundColorConfig = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.selected,.highlighted]) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) }{ didSet { setNeedsUpdate() }} open var borderColorConfig = 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) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) }{ didSet { setNeedsUpdate() }} open var selectorColorConfig = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .selected) $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: [.selected, .disabled]) $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: [.selected, .highlighted]) }{ didSet { setNeedsUpdate() }} //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var selectorHeightConstraint: NSLayoutConstraint? private var selectorWidthConstraint: NSLayoutConstraint? private var shapeLayer: CAShapeLayer? open override func setup() { super.setup() let layoutGuide = UILayoutGuide() addLayoutGuide(layoutGuide) selectorHeightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: size.height) selectorHeightConstraint?.isActive = true selectorWidthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: size.width) selectorWidthConstraint?.isActive = true NSLayoutConstraint.activate([ layoutGuide.topAnchor.constraint(equalTo: topAnchor), layoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), layoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), layoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor)]) layer.cornerRadius = 2.0 layer.borderWidth = VDSFormControls.widthBorder } open override func updateView() { super.updateView() selectorHeightConstraint?.constant = size.height selectorWidthConstraint?.constant = size.width setNeedsLayout() layoutIfNeeded() } open override func layoutSubviews() { super.layoutSubviews() //get the colors let backgroundColor = backgroundColorConfig.getColor(self) let borderColor = borderColorConfig.getColor(self) let checkColor = selectorColorConfig.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.widthBorder 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 = checkColor.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 && !disabled && !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 } } }