initial cut for the checkbox

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-06-28 14:57:15 -05:00
parent eb78d507d6
commit 11a023d92a
5 changed files with 70 additions and 409 deletions

View File

@ -7,113 +7,44 @@
//
import MVMCore
import VDS
/**
This class expects its height and width to be equal.
*/
@objcMembers open class Checkbox: Control, MVMCoreUIViewConstrainingProtocol {
//--------------------------------------------------
@objcMembers open class Checkbox: VDS.Checkbox, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol {
//------------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var sizeObject: MFSizeObject? = MFSizeObject(standardSize: Checkbox.defaultHeightWidth, standardiPadPortraitSize: Checkbox.defaultHeightWidth + 6.0)
//------------------------------------------------------
open var viewModel: CheckboxModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
// Form Validation
var fieldKey: String?
var fieldValue: JSONValue?
var groupName: String?
var delegateObject: MVMCoreUIDelegateObject?
public var checkboxModel: CheckboxModel? {
model as? CheckboxModel
viewModel
}
public static let defaultHeightWidth: CGFloat = 18.0
/// If true the border of this checkbox will be circular.
public var isRound: Bool = false
/// Determined if the checkbox's UI should animated when selected.
public var isAnimated: Bool = true
/// Disables all selection logic when setting the value of isSelected, reducing it to a stored property.
public var updateSelectionOnly: Bool = false
/// The color of the background when checked.
public var checkedBackgroundColor: UIColor = .clear {
didSet {
if isSelected {
backgroundColor = checkedBackgroundColor
}
}
}
/// The color of the background when unChecked.
public var unCheckedBackgroundColor: UIColor = .clear {
didSet {
if !isSelected {
backgroundColor = unCheckedBackgroundColor
}
}
}
/// Retrieves ideeal radius value to curve square into a circle.
public var cornerRadiusValue: CGFloat {
bounds.size.height / 2
}
/// Action Block called when the switch is selected.
public var actionBlock: ActionBlock?
/// Manages the appearance of the checkbox.
private var shapeLayer: CAShapeLayer?
/// Width of the check mark.
public var checkWidth: CGFloat = 2 {
didSet {
if let shapeLayer = shapeLayer {
CATransaction.withDisabledAnimations {
shapeLayer.lineWidth = checkWidth
public var actionBlock: ActionBlock? {
get { nil }
set {
if let action = newValue {
onChange = { _ in
action()
}
}
}
}
open override var isEnabled: Bool {
didSet {
isUserInteractionEnabled = isEnabled
if isEnabled {
layer.borderColor = borderColor.cgColor
backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
setShapeLayerStrokeColor(checkColor)
} else {
layer.borderColor = disabledBorderColor.cgColor
backgroundColor = disabledBackgroundColor
setShapeLayerStrokeColor(disabledCheckColor)
onChange = nil
}
}
}
public var disabledBackgroundColor: UIColor = .clear
public var disabledBorderColor: UIColor = .mvmCoolGray3
public var disabledCheckColor: UIColor = .mvmCoolGray3
/// Color of the check mark.
public var checkColor: UIColor = .mvmBlack {
didSet { setShapeLayerStrokeColor(checkColor) }
}
/// Border width of the checkbox
public var borderWidth: CGFloat = 1 {
didSet { layer.borderWidth = borderWidth }
}
/// border color of the Checkbox
public var borderColor: UIColor = .mvmBlack {
didSet { layer.borderColor = borderColor.cgColor }
}
/**
The represented state of the Checkbox.
@ -123,42 +54,18 @@ import MVMCore
override open var isSelected: Bool {
didSet {
if !updateSelectionOnly {
layoutIfNeeded()
(model as? CheckboxModel)?.selected = isSelected
shapeLayer?.removeAllAnimations()
updateCheckboxUI(isSelected: isSelected, isAnimated: isAnimated)
viewModel.selected = isSelected
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
updateAccessibilityLabel()
}
}
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
public var heightConstraint: NSLayoutConstraint?
public var widthConstraint: NSLayoutConstraint?
/// Updates the height and width anchors of the Checkbox with the assigned value.
public var heigthWidthConstant: CGFloat = Checkbox.defaultHeightWidth {
didSet {
heightConstraint?.constant = heigthWidthConstant
widthConstraint?.constant = heigthWidthConstant
}
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
override public init(frame: CGRect) {
super.init(frame: frame)
isAccessibilityElement = true
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint")
accessibilityTraits = .button
updateAccessibilityLabel()
}
/// There is currently no intention on using xib files.
@ -167,122 +74,61 @@ import MVMCore
fatalError("xib file is not implemented for Checkbox.")
}
public convenience override init() {
public convenience required init() {
self.init(frame:.zero)
}
public convenience init(isChecked: Bool) {
self.init(frame: .zero)
checkAndBypassAnimations(selected: isChecked)
isSelected = isChecked
}
public convenience init(checkedBackgroundColor: UIColor, unCheckedBackgroundColor: UIColor, isChecked: Bool = false) {
self.init(frame: .zero)
checkAndBypassAnimations(selected: isChecked)
self.checkedBackgroundColor = checkedBackgroundColor
self.unCheckedBackgroundColor = unCheckedBackgroundColor
isSelected = isChecked
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
override open func layoutSubviews() {
super.layoutSubviews()
open override func setup() {
super.setup()
bridge_accessibilityLabelBlock = { [weak self] in
// Attention: This needs to be addressed with the accessibility team.
// NOTE: Currently emptying description part of MVMCoreUICheckBox accessibility label to avoid crashing!
guard let self,
let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state")
else { return nil }
return String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state") ?? "%@%@", "", state)
}
drawShapeLayer()
layer.cornerRadius = isRound ? cornerRadiusValue : 0
}
open override func setupView() {
super.setupView()
isUserInteractionEnabled = true
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .clear
widthConstraint = widthAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
heightConstraint = heightAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
heightWidthIsActive(true)
bridge_accessibilityHintBlock = {
MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint")
}
}
//--------------------------------------------------
// MARK: - Actions
//--------------------------------------------------
open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
super.sendAction(action, to: target, for: event)
toggleAndAction()
}
open override func sendActions(for controlEvents: UIControl.Event) {
super.sendActions(for: controlEvents)
toggleAndAction()
}
/// This will toggle the state of the Checkbox and execute the actionBlock if provided.
public func toggleAndAction() {
isSelected.toggle()
actionBlock?()
toggle()
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
/// Creates the check mark layer.
private func drawShapeLayer() {
if shapeLayer == nil {
let shapeLayer = CAShapeLayer()
self.shapeLayer = shapeLayer
shapeLayer.frame = bounds
layer.addSublayer(shapeLayer)
shapeLayer.strokeColor = isEnabled ? checkColor.cgColor : disabledCheckColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = checkMarkPath()
shapeLayer.lineJoin = .miter
shapeLayer.lineWidth = checkWidth
CATransaction.withDisabledAnimations {
shapeLayer.strokeEnd = isSelected ? 1 : 0
}
}
}
/// - 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
}
/// Programmatic means to check/uncheck the box.
/// - parameter selected: state of the check box: true = checked OR false = unchecked.
/// - parameter animated: allows the state of the checkbox to change with or without animation.
public func updateSelection(to selected: Bool, animated: Bool) {
DispatchQueue.main.async {
self.checkAndBypassAnimations(selected: selected)
self.drawShapeLayer()
self.shapeLayer?.removeAllAnimations()
self.updateCheckboxUI(isSelected: selected, isAnimated: animated)
self.isAnimated = animated
self.isSelected = selected
}
}
@ -291,154 +137,59 @@ import MVMCore
/// - parameter isAnimated: determines of the changes should animate or immediately refelect.
public func updateCheckboxUI(isSelected: Bool, isAnimated: Bool) {
if isAnimated {
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 = isSelected ? self.checkedBackgroundColor : self.unCheckedBackgroundColor
})
} else {
CATransaction.withDisabledAnimations {
self.shapeLayer?.strokeEnd = isSelected ? 1 : 0
}
backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
DispatchQueue.main.async {
self.isAnimated = isAnimated
self.isSelected = isSelected
}
}
/// Adjust accessibility label based on state of Checkbox.
public func updateAccessibilityLabel() {
// Attention: This needs to be addressed with the accessibility team.
// NOTE: Currently emptying description part of MVMCoreUICheckBox accessibility label to avoid crashing!
if let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state") {
accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state") ?? "%@%@", "", state)
}
}
private func setShapeLayerStrokeColor(_ color: UIColor) {
if let shapeLayer = shapeLayer {
CATransaction.withDisabledAnimations {
shapeLayer.strokeColor = color.cgColor
}
}
}
public func heightWidthIsActive(_ isActive: Bool) {
heightConstraint?.isActive = isActive
widthConstraint?.isActive = isActive
}
private func checkAndBypassAnimations(selected: Bool) {
updateSelectionOnly = true
isSelected = selected
updateSelectionOnly = false
}
//--------------------------------------------------
// MARK: - UITouch
//--------------------------------------------------
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
sendActions(for: .touchUpInside)
}
override open func accessibilityActivate() -> Bool {
guard isEnabled else { return false }
sendActions(for: .touchUpInside)
return true
}
//--------------------------------------------------
// MARK: - Molecular
//--------------------------------------------------
open func needsToBeConstrained() -> Bool { true }
open override func reset() {
super.reset()
isEnabled = true
shapeLayer?.removeAllAnimations()
shapeLayer?.removeFromSuperlayer()
shapeLayer = nil
backgroundColor = .clear
borderColor = .mvmBlack
borderWidth = 1
checkColor = .mvmBlack
checkWidth = 2
checkAndBypassAnimations(selected: false)
}
public override func updateView(_ size: CGFloat) {
super.updateView(size)
if let dimension = sizeObject?.getValueBased(onSize: size) {
widthConstraint?.constant = dimension
heightConstraint?.constant = dimension
}
}
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
public func updateView(_ size: CGFloat) {}
private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: checkboxModel, additionalData: additionalData, delegateObject: delegateObject)
}
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
public func viewModelDidUpdate() {
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
guard let model = model as? CheckboxModel else { return }
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
if let fieldKey = model.fieldKey {
if let fieldKey = viewModel.fieldKey {
self.fieldKey = fieldKey
}
borderColor = (model.inverted ? model.invertedColor : model.borderColor).uiColor
borderWidth = model.borderWidth
checkColor = (model.inverted ? model.invertedColor : model.checkColor).uiColor
unCheckedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.unCheckedBackgroundColor).uiColor
checkedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.checkedBackgroundColor).uiColor
disabledCheckColor = (model.inverted ? model.invertedColor : model.disabledCheckColor).uiColor
disabledBorderColor = (model.inverted ? model.invertedColor : model.disabledBorderColor).uiColor
disabledBackgroundColor = (model.inverted ? model.invertedColor : model.disabledBackgroundColor).uiColor
isAnimated = model.animated
isRound = model.round
if model.selected {
checkAndBypassAnimations(selected: model.selected)
isAnimated = viewModel.animated
if viewModel.selected {
updateSelectionOnly = true
isSelected = viewModel.selected
updateSelectionOnly = false
}
model.updateUI = { [weak self] in
MVMCoreDispatchUtility.performBlock(onMainThread: {
viewModel.updateUI = {
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
guard let self = self else { return }
self.isEnabled = model.enabled
isEnabled = viewModel.enabled
})
}
isEnabled = model.enabled && !model.readOnly
isEnabled = viewModel.enabled && !viewModel.readOnly
if (model.action != nil || model.offAction != nil) {
if (viewModel.action != nil || viewModel.offAction != nil) {
actionBlock = { [weak self] in
guard let self = self else { return }
if let offAction = model.offAction, !self.isSelected {
self.performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
if let offAction = viewModel.offAction, !isSelected {
performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
} else if let action = model.action {
self.performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
} else if let action = viewModel.action {
performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
}
}
}

View File

@ -5,6 +5,7 @@
// Created by Chintakrinda, Arun Kumar (Arun) on 21/01/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import VDS
/// Protocol to apply to any model of a UI Control with a binary on/off nature.
///
@ -28,20 +29,10 @@
public var readOnly: Bool = false
public var animated: Bool = true
public var inverted: Bool = false
public var round: Bool = false
public var borderWidth: CGFloat = 1
public var borderColor: Color = Color(uiColor: .mvmBlack)
public var checkColor: Color = Color(uiColor: .mvmBlack)
public var unCheckedBackgroundColor: Color = Color(uiColor: .clear)
public var checkedBackgroundColor: Color = Color(uiColor: .clear)
public var disabledBackgroundColor: Color = Color(uiColor: .clear)
public var disabledBorderColor: Color = Color(uiColor: .mvmCoolGray3)
public var disabledCheckColor: Color = Color(uiColor: .mvmCoolGray3)
public var invertedColor: Color = Color(uiColor: .mvmWhite)
public var invertedBackgroundColor: Color = Color(uiColor: .mvmBlack)
public var action: ActionModelProtocol?
public var offAction: ActionModelProtocol?
public var surface: Surface { inverted ? .dark : .light }
public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName
public var baseValue: AnyHashable?
@ -60,17 +51,6 @@
case readOnly
case inverted
case animated
case round
case borderWidth
case borderColor
case checkColor
case invertedColor
case invertedBackgroundColor
case unCheckedBackgroundColor
case checkedBackgroundColor
case disabledBackgroundColor
case disabledCheckColor
case disabledBorderColor
case action
case fieldKey
case groupName
@ -113,46 +93,6 @@
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
if let borderWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .borderWidth) {
self.borderWidth = borderWidth
}
if let borderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .borderColor) {
self.borderColor = borderColor
}
if let checkColor = try typeContainer.decodeIfPresent(Color.self, forKey: .checkColor) {
self.checkColor = checkColor
}
if let unCheckedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .unCheckedBackgroundColor) {
self.unCheckedBackgroundColor = unCheckedBackgroundColor
}
if let checkedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .checkedBackgroundColor) {
self.checkedBackgroundColor = checkedBackgroundColor
}
if let disabledBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBackgroundColor) {
self.disabledBackgroundColor = disabledBackgroundColor
}
if let disabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBorderColor) {
self.disabledBorderColor = disabledBorderColor
}
if let disabledCheckColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledCheckColor) {
self.disabledCheckColor = disabledCheckColor
}
if let invertedColor = try typeContainer.decodeIfPresent(Color.self, forKey: .invertedColor) {
self.invertedColor = invertedColor
}
if let invertedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .invertedBackgroundColor) {
self.invertedBackgroundColor = invertedBackgroundColor
}
if let checked = try typeContainer.decodeIfPresent(Bool.self, forKey: .checked) {
self.selected = checked
}
@ -162,11 +102,7 @@
if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) {
self.animated = animated
}
if let round = try typeContainer.decodeIfPresent(Bool.self, forKey: .round) {
self.round = round
}
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
}
@ -188,21 +124,10 @@
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(groupName, forKey: .groupName)
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
try container.encodeIfPresent(borderColor, forKey: .borderColor)
try container.encode(borderWidth, forKey: .borderWidth)
try container.encode(selected, forKey: .checked)
try container.encode(inverted, forKey: .inverted)
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
try container.encodeIfPresent(checkColor, forKey: .checkColor)
try container.encodeIfPresent(invertedColor, forKey: .invertedColor)
try container.encodeIfPresent(invertedBackgroundColor, forKey: .invertedBackgroundColor)
try container.encodeIfPresent(unCheckedBackgroundColor, forKey: .unCheckedBackgroundColor)
try container.encodeIfPresent(checkedBackgroundColor, forKey: .checkedBackgroundColor)
try container.encodeIfPresent(disabledBorderColor, forKey: .disabledBorderColor)
try container.encodeIfPresent(disabledBackgroundColor, forKey: .disabledBackgroundColor)
try container.encodeIfPresent(disabledCheckColor, forKey: .disabledCheckColor)
try container.encodeIfPresent(animated, forKey: .animated)
try container.encodeIfPresent(round, forKey: .round)
try container.encode(enabled, forKey: .enabled)
try container.encode(readOnly, forKey: .readOnly)
try container.encodeModelIfPresent(action, forKey: .action)

View File

@ -36,7 +36,7 @@
override open func setupView() {
super.setupView()
guard subviews.isEmpty else { return }
addSubview(checkbox)
@ -66,9 +66,6 @@
alignCheckbox(.center)
isAccessibilityElement = false
accessibilityElements = [checkbox, label]
observation = observe(\.checkbox.isSelected, options: [.new]) { [weak self] _, _ in
self?.updateAccessibilityLabel()
}
}
@objc override open func updateView(_ size: CGFloat) {
@ -117,7 +114,6 @@
checkbox.set(with: checkBoxWithLabelModel.checkbox, delegateObject, additionalData)
label.set(with: checkBoxWithLabelModel.label, delegateObject, additionalData)
updateAccessibilityLabel()
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
@ -135,11 +131,4 @@
override open func accessibilityActivate() -> Bool {
checkbox.accessibilityActivate()
}
open func updateAccessibilityLabel() {
checkbox.updateAccessibilityLabel()
if let text = label.text {
checkbox.accessibilityLabel?.append(", \(text)")
}
}
}

View File

@ -83,9 +83,7 @@
func updateAccessibilityLabel() {
var message = ""
checkbox.updateAccessibilityLabel()
if let checkboxLabel = checkbox.accessibilityLabel, !checkboxLabel.isEmpty {
message += checkboxLabel + ", "
}

View File

@ -88,9 +88,7 @@
func updateAccessibilityLabel() {
var message = ""
checkbox.updateAccessibilityLabel()
if let checkboxLabel = checkbox.accessibilityLabel {
message += checkboxLabel + ", "
}