From d8a55d87bcb42e293d0bff90c4e74dc8a6e73236 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 2 Aug 2022 16:22:55 -0500 Subject: [PATCH] updated checkbox Signed-off-by: Matt Bruce --- VDS/Components/Checkbox/VDSCheckbox.swift | 200 +++++++++++------- .../Checkbox/VDSCheckboxModel.swift | 6 +- 2 files changed, 127 insertions(+), 79 deletions(-) diff --git a/VDS/Components/Checkbox/VDSCheckbox.swift b/VDS/Components/Checkbox/VDSCheckbox.swift index f34105da..95a17a29 100644 --- a/VDS/Components/Checkbox/VDSCheckbox.swift +++ b/VDS/Components/Checkbox/VDSCheckbox.swift @@ -26,18 +26,29 @@ import Combine //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - private func getCheckboxTintColor(for disabled: Bool, surface: Surface) -> (on: UIColor, off: UIColor) { + private func getCheckboxBackgroundColor(for disabled: Bool, surface: Surface) -> (on: UIColor, off: UIColor) { if disabled { if surface == .light { - return (on: VDSColor.interactiveDisabledOnlight, off: UIColor.clear) + return ( + on: VDSColor.interactiveDisabledOnlight, + off: .clear) } else { - return (on: VDSColor.interactiveDisabledOndark, off: UIColor.clear) + return ( + on: VDSColor.interactiveDisabledOndark, + off: .clear + ) } } else { if surface == .light { - return (on: VDSColor.elementsPrimaryOnlight, off: UIColor.clear) + return ( + on: VDSColor.elementsPrimaryOnlight, + off: .clear + ) } else { - return (on: VDSColor.elementsPrimaryOndark, off: UIColor.clear) + return ( + on: VDSColor.elementsPrimaryOndark, + off: .clear + ) } } } @@ -45,15 +56,27 @@ import Combine private func getCheckboxBorderColor(for disabled: Bool, surface: Surface) -> (on: UIColor, off: UIColor) { if disabled { if surface == .light { - return (on: VDSColor.interactiveDisabledOnlight, off: VDSColor.interactiveDisabledOnlight) + return ( + on: VDSColor.interactiveDisabledOnlight, + off: VDSColor.interactiveDisabledOnlight + ) } else { - return (on: VDSColor.interactiveDisabledOnlight, off: VDSColor.interactiveDisabledOnlight) + return ( + on: VDSColor.interactiveDisabledOndark, + off: VDSColor.interactiveDisabledOnlight + ) } } else { if surface == .light { - return (on: VDSColor.elementsPrimaryOndark, off: VDSFormControlsColor.borderOnlight) + return ( + on: VDSColor.elementsPrimaryOnlight, + off: VDSFormControlsColor.borderOnlight + ) } else { - return (on: VDSColor.elementsPrimaryOndark, off: VDSFormControlsColor.borderOndark) + return ( + on: VDSColor.elementsPrimaryOndark, + off: VDSFormControlsColor.borderOndark + ) } } } @@ -61,7 +84,7 @@ import Combine private func getCheckboxCheckColor(for disabled: Bool, surface: Surface) -> UIColor { if disabled { if surface == .light { - return VDSColor.interactiveDisabledOnlight + return VDSColor.interactiveDisabledOndark } else { return VDSColor.interactiveDisabledOnlight } @@ -69,7 +92,7 @@ import Combine if surface == .light { return VDSColor.elementsPrimaryOndark } else { - return VDSColor.elementsPrimaryOndark + return VDSColor.elementsPrimaryOnlight } } } @@ -78,7 +101,7 @@ import Combine let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical - stackView.distribution = .fillProportionally + stackView.distribution = .fill return stackView }() @@ -117,8 +140,8 @@ import Combine return label }() - private var checkboxView: UIView = { - let view = UIView() + private var checkboxView: CheckBoxView = { + let view = CheckBoxView() view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -132,7 +155,7 @@ import Combine // MARK: - Static Properties //-------------------------------------------------- // Sizes are from InVision design specs. - public let checkboxSize = CGSize(width: 52, height: 24) + public let checkboxSize = CGSize(width: 20, height: 20) //-------------------------------------------------- // MARK: - Computed Properties @@ -161,7 +184,7 @@ import Combine @Proxy(\.model.surface) public var surface: Surface - @Proxy(\.model.selected) + @Proxy(\.model.on) open var isOn: Bool @Proxy(\.model.disabled) @@ -242,61 +265,20 @@ import Combine mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } - - /// Manages the appearance of the checkbox. - private var shapeLayer: CAShapeLayer? /// Creates the check mark layer. private func setCheckboxColor(viewModel: ModelType) { - let background = getCheckboxTintColor(for: viewModel.disabled, surface: viewModel.surface) + let background = getCheckboxBackgroundColor(for: viewModel.disabled, surface: viewModel.surface) let border = getCheckboxBorderColor(for: viewModel.disabled, surface: viewModel.surface) let checkColor = getCheckboxCheckColor(for: viewModel.disabled, surface: viewModel.surface) - checkboxView.backgroundColor = viewModel.disabled ? background.off : background.on - checkboxView.layer.borderColor = (viewModel.disabled ? border.off : border.on).cgColor + checkboxView.backgroundColor = viewModel.on ? background.on : background.off + checkboxView.borderColor = viewModel.on ? border.on : border.off + checkboxView.checkColor = checkColor + checkboxView.isSelected = viewModel.on + } - if shapeLayer == nil { - - let shapeLayer = CAShapeLayer() - self.shapeLayer = shapeLayer - shapeLayer.frame = bounds - checkboxView.layer.addSublayer(shapeLayer) - shapeLayer.strokeColor = checkColor.cgColor - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.path = checkMarkPath() - shapeLayer.lineJoin = .miter - shapeLayer.lineWidth = 2 - - CATransaction.withDisabledAnimations { - shapeLayer.strokeEnd = viewModel.selected ? 1 : 0 - } - } else { - shapeLayer?.strokeColor = checkColor.cgColor - } - } - - /// - returns: The CGPath of a UIBezierPath detailing the path of a checkmark - func checkMarkPath() -> CGPath { - - let length = max(bounds.size.height, bounds.size.width) - 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) - - return bezierPath.cgPath - } - func ensureLabel(viewModel: ModelType) { //deal with labels @@ -308,8 +290,8 @@ import Combine //top label if let labelModel = viewModel.labelModel { primaryLabel.set(with: labelModel) - if checkBoxStackView.subviews.contains(primaryLabel) == false { - checkBoxStackView.insertArrangedSubview(primaryLabel, at: 0) + if checkboxLabelStackView.subviews.contains(primaryLabel) == false { + checkboxLabelStackView.insertArrangedSubview(primaryLabel, at: 0) } } else { primaryLabel.removeFromSuperview() @@ -317,8 +299,8 @@ import Combine //bottom label if let childModel = viewModel.childModel { secondaryLabel.set(with: childModel) - if checkBoxStackView.subviews.contains(secondaryLabel) == false { - checkBoxStackView.addArrangedSubview(secondaryLabel) + if checkboxLabelStackView.subviews.contains(secondaryLabel) == false { + checkboxLabelStackView.addArrangedSubview(secondaryLabel) } } else { secondaryLabel.removeFromSuperview() @@ -328,9 +310,12 @@ import Combine } //either add/remove the error from the main stack - if model.shouldShowError { - mainStackView.spacing = 12 - mainStackView.addArrangedSubview(errorLabel) + if let errorModel = model.errorModel, model.shouldShowError { + errorLabel.set(with: errorModel) + if mainStackView.subviews.contains(errorLabel) == false { + mainStackView.spacing = 12 + mainStackView.addArrangedSubview(errorLabel) + } } else { mainStackView.spacing = 0 errorLabel.removeFromSuperview() @@ -380,10 +365,6 @@ import Combine sendActions(for: .touchUpInside) } - open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - sendActions(for: .touchCancel) - } - //-------------------------------------------------- // MARK: - Animations //-------------------------------------------------- @@ -396,11 +377,78 @@ import Combine ensureLabel(viewModel: viewModel) setCheckboxColor(viewModel: viewModel) setAccessibilityHint(enabled) - setAccessibilityValue(viewModel.selected) - setAccessibilityLabel(viewModel.selected) + setAccessibilityValue(viewModel.on) + setAccessibilityLabel(viewModel.on) isUserInteractionEnabled = !viewModel.disabled setNeedsLayout() layoutIfNeeded() } + + private class CheckBoxView: UIView { + public var borderColor: UIColor = .black + public var checkColor: UIColor = .white + public var isSelected: Bool = false { + didSet { + drawShapeLayer() + } + } + + /// Manages the appearance of the checkbox. + private var shapeLayer: CAShapeLayer? + + /// Creates the check mark layer. + private func drawShapeLayer() { + layer.borderColor = borderColor.cgColor + layer.cornerRadius = 2.0 + shapeLayer?.strokeColor = checkColor.cgColor + + guard let path = try? checkMarkPath() else { return } + + if shapeLayer == nil { + let shapeLayer = CAShapeLayer() + self.shapeLayer = shapeLayer + shapeLayer.frame = bounds + layer.addSublayer(shapeLayer) + shapeLayer.strokeColor = checkColor.cgColor + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.path = path + shapeLayer.lineJoin = .miter + shapeLayer.lineWidth = 2 + CATransaction.withDisabledAnimations { + shapeLayer.strokeEnd = isSelected ? 1 : 0 + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + drawShapeLayer() + } + + /// - returns: The CGPath of a UIBezierPath detailing the path of a checkmark + private func checkMarkPath() throws -> CGPath { + let length = max(bounds.size.height, bounds.size.width) + guard length > 0.0 else { throw Error.boundsNotSet } + 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) + + return bezierPath.cgPath + } + + enum Error: Swift.Error { + case boundsNotSet + } + } } diff --git a/VDS/Components/Checkbox/VDSCheckboxModel.swift b/VDS/Components/Checkbox/VDSCheckboxModel.swift index 90e29e98..609d5973 100644 --- a/VDS/Components/Checkbox/VDSCheckboxModel.swift +++ b/VDS/Components/Checkbox/VDSCheckboxModel.swift @@ -15,7 +15,7 @@ public protocol Errorable { public protocol VDSCheckboxModel: FormFieldable, Errorable, DataTrackable, Accessable, Surfaceable, Disabling, Initable { var id: String? { get set } - var selected: Bool { get set } + var on: Bool { get set } var labelText: String? { get set } var childText: String? { get set } } @@ -32,7 +32,7 @@ extension VDSCheckboxModel { } public var shouldShowLabels: Bool { - guard labelText?.isEmpty == false && childText?.isEmpty == false else { return false } + guard labelText?.isEmpty == false || childText?.isEmpty == false else { return false } return true } @@ -78,7 +78,7 @@ extension VDSCheckboxModel { public class DefaultCheckboxModel: VDSCheckboxModel { public var id: String? - public var selected: Bool = false + public var on: Bool = false public var labelText: String? public var childText: String?