// // Checkbox.swift // VDS // // Created by Matt Bruce on 7/22/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine public class Checkbox: CheckboxBase{} public class SoloCheckbox: CheckboxBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } open class CheckboxBase: Control, Accessable, BinaryColorable, Errorable { //-------------------------------------------------- // 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: - Private Properties //-------------------------------------------------- private var shouldShowError: Bool { guard showError && !disabled && errorText?.isEmpty == false else { return false } return true } private var shouldShowLabels: Bool { guard labelText?.isEmpty == false || childText?.isEmpty == false else { return false } return true } private var mainStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .vertical } }() private var selectorStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .horizontal } }() private var selectorLabelStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical } }() private var primaryLabel = Label() private var secondaryLabel = Label() private var errorLabel = Label() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var selectorView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() //can't bind to @Proxy open override var isSelected: Bool { didSet { didChange() }} open var labelText: String? { didSet { didChange() }} open var labelTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} open var childText: String? { didSet { didChange() }} open var childTextAttributes: [any LabelAttributeModel]? { didSet { didChange() }} open var showError: Bool = false { didSet { didChange() }} open var errorText: String? { 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() }} private var labelModel: DefaultLabelModel? { guard let labelText = labelText else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BoldBodyLarge model.text = labelText model.surface = surface model.disabled = disabled model.attributes = labelTextAttributes return model } private var childModel: DefaultLabelModel? { guard let childText = childText else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BodyLarge model.text = childText model.surface = surface model.disabled = disabled model.attributes = childTextAttributes return model } private var errorModel: DefaultLabelModel? { guard let errorText = errorText, showError else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BodyMedium model.text = errorText model.surface = surface model.disabled = disabled return model } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var selectorHeightConstraint: NSLayoutConstraint? private var selectorWidthConstraint: NSLayoutConstraint? //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(mainStackView) mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) selectorStackView.addArrangedSubview(selectorLabelStackView) selectorLabelStackView.addArrangedSubview(primaryLabel) selectorLabelStackView.addArrangedSubview(secondaryLabel) let selectorSize = getSelectorSize() selectorHeightConstraint = selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height) selectorHeightConstraint?.isActive = true selectorWidthConstraint = selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width) selectorWidthConstraint?.isActive = true updateSelector() mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } func updateLabels() { //deal with labels if shouldShowLabels { //add the stackview to hold the 2 labels //top label if let labelText { primaryLabel.textPosition = .left primaryLabel.typograpicalStyle = .BoldBodyLarge primaryLabel.text = labelText primaryLabel.surface = surface primaryLabel.disabled = disabled primaryLabel.attributes = labelTextAttributes primaryLabel.isHidden = false } else { primaryLabel.isHidden = true } //bottom label if let childText { secondaryLabel.textPosition = .left secondaryLabel.typograpicalStyle = .BodyLarge secondaryLabel.text = childText secondaryLabel.surface = surface secondaryLabel.disabled = disabled secondaryLabel.attributes = childTextAttributes secondaryLabel.isHidden = false } else { secondaryLabel.isHidden = true } selectorStackView.spacing = 12 selectorLabelStackView.spacing = 4 selectorLabelStackView.isHidden = false } else { selectorStackView.spacing = 0 selectorLabelStackView.spacing = 0 selectorLabelStackView.isHidden = true } //either add/remove the error from the main stack if let errorText, shouldShowError { errorLabel.textPosition = .left errorLabel.typograpicalStyle = .BodyMedium errorLabel.text = errorText errorLabel.surface = surface errorLabel.disabled = disabled mainStackView.spacing = 8 errorLabel.isHidden = false } else { mainStackView.spacing = 0 errorLabel.isHidden = true } } public override func reset() { super.reset() updateSelector() setAccessibilityLabel() } /// This will checkbox the state of the Selector and execute the actionBlock if provided. open func toggle() { //removed error if showError && isSelected == false { showError.toggle() } isSelected.toggle() sendActions(for: .valueChanged) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { updateLabels() updateSelector() setAccessibilityHint() setAccessibilityValue(isSelected) setAccessibilityLabel(isSelected) } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- public let checkboxSize = CGSize(width: 20, height: 20) private var checkboxBackgroundColorConfiguration: ErrorBinaryDisabledSurfaceColorConfiguration = { return ErrorBinaryDisabledSurfaceColorConfiguration().with { $0.forTrue.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.forTrue.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.forTrue.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.forTrue.disabled.darkColor = VDSColor.interactiveDisabledOndark //error doesn't care enabled/disable $0.error.forTrue.lightColor = VDSColor.elementsPrimaryOnlight $0.error.forTrue.darkColor = VDSColor.elementsPrimaryOndark $0.error.forFalse.lightColor = VDSColor.feedbackErrorBackgroundOnlight $0.error.forFalse.darkColor = VDSColor.feedbackErrorBackgroundOndark } }() private var checkboxBorderColorConfiguration: ErrorBinaryDisabledSurfaceColorConfiguration = { return ErrorBinaryDisabledSurfaceColorConfiguration().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 //error doesn't care enabled/disable $0.error.forTrue.lightColor = VDSColor.elementsPrimaryOnlight $0.error.forTrue.darkColor = VDSColor.elementsPrimaryOndark $0.error.forFalse.lightColor = VDSColor.feedbackErrorOnlight $0.error.forFalse.darkColor = VDSColor.feedbackErrorOndark } }() private var checkboxCheckColorConfiguration: BinarySurfaceColorConfiguration = { return BinarySurfaceColorConfiguration().with { $0.forTrue.lightColor = VDSColor.elementsPrimaryOndark $0.forTrue.darkColor = VDSColor.elementsPrimaryOnlight } }() //-------------------------------------------------- // MARK: - Checkbox View //-------------------------------------------------- /// Manages the appearance of the checkbox. private var shapeLayer: CAShapeLayer? open func getSelectorSize() -> CGSize { return checkboxSize } open func updateSelector() { //get the colors let backgroundColor = checkboxBackgroundColorConfiguration.getColor(self) let borderColor = checkboxBorderColorConfiguration.getColor(self) let checkColor = checkboxCheckColorConfiguration.getColor(self) if let shapeLayer = shapeLayer, let sublayers = layer.sublayers, sublayers.contains(shapeLayer) { shapeLayer.removeFromSuperlayer() self.shapeLayer = nil } selectorView.backgroundColor = backgroundColor selectorView.layer.borderColor = borderColor.cgColor selectorView.layer.cornerRadius = 2.0 selectorView.layer.borderWidth = 1.0 if shapeLayer == nil { let bounds = selectorView.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 } } } } //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- internal class ErrorBinaryDisabledSurfaceColorConfiguration: BinaryDisabledSurfaceColorable { typealias ModelType = Errorable & Disabling & Surfaceable & BinaryColorable var error = BinarySurfaceColorConfiguration() var forTrue = DisabledSurfaceColorConfiguration() var forFalse = DisabledSurfaceColorConfiguration() required init() {} func getColor(_ viewModel: ModelType) -> UIColor { //only show error is enabled and showError == true let showErrorColor = !viewModel.disabled && viewModel.showError if showErrorColor { return error.getColor(viewModel) } else { return getBinaryColor(viewModel) } } }