From f5f994648aca33f941fead0178d8ed4fed149c91 Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Fri, 27 Sep 2019 13:43:02 -0400 Subject: [PATCH] Decent working state with animation and touch. --- MVMCoreUI/Atoms/Views/Checkbox.swift | 270 ++++++++++++------ .../Atoms/Views/CheckboxWithLabelView.swift | 101 ++----- 2 files changed, 212 insertions(+), 159 deletions(-) diff --git a/MVMCoreUI/Atoms/Views/Checkbox.swift b/MVMCoreUI/Atoms/Views/Checkbox.swift index 1ab976ef..d00c2463 100644 --- a/MVMCoreUI/Atoms/Views/Checkbox.swift +++ b/MVMCoreUI/Atoms/Views/Checkbox.swift @@ -16,36 +16,54 @@ import MVMCore public static let defaultHeightWidth: CGFloat = 18.0 - /// The color of the box and line when checked. - public var checkedColor: UIColor = .black + /// The color of the background when checked. + public var checkedBackgroundColor: UIColor = .white - /// The color of the border when unChecked. - public var unCheckedColor: UIColor = .black + /// The color of the background when unChecked. + public var unCheckedBackgroundColor: UIColor = .black /// If true the border of this checkbox will be circular. public var hasRoundCorners = false + // Action Block called when the switch is selected. + public var actionBlock: ActionBlock? + // Internal values to manage the appearance of the checkbox. private var shapeLayer: CAShapeLayer? public var cornerRadiusValue: CGFloat { - return bounds.height / 2 + return bounds.size.height / 2 } - public var lineWidth: CGFloat = 1 + public var lineWidth: CGFloat = 2 public var lineColor: UIColor = .black - public var borderColor: UIColor = .black - public var checkedBackgroundColor: UIColor = .white + + open var borderColor: UIColor { + get { + guard let color = layer.borderColor else { return .black } + return UIColor(cgColor: color) + } + set (newColor) { + layer.borderColor = newColor.cgColor + } + } + open var borderWidth: CGFloat { + get { return layer.borderWidth } + set (newWidth) { + layer.borderWidth = newWidth + } + } + override open var isSelected: Bool { didSet { if isSelected { - layer.addSublayer(shapeLayer!) - shapeLayer?.strokeEnd = 1 +// layer.addSublayer(shapeLayer!) +// shapeLayer?.strokeEnd = 1 shapeLayer?.removeAllAnimations() shapeLayer?.add(checkedAnimation, forKey: "strokeEnd") } else { - shapeLayer?.strokeEnd = 0 +// shapeLayer?.strokeEnd = 0 shapeLayer?.removeAllAnimations() shapeLayer?.add(uncheckedAnimation, forKey: "strokeEnd") } @@ -55,8 +73,9 @@ import MVMCore lazy private var checkedAnimation: CABasicAnimation = { let check = CABasicAnimation(keyPath: "strokeEnd") check.timingFunction = CAMediaTimingFunction(name: .linear) + check.isRemovedOnCompletion = false check.fillMode = .both - check.duration = 0.33 + check.duration = 0.3 check.fromValue = 0 check.toValue = 1 return check @@ -65,10 +84,11 @@ import MVMCore lazy private var uncheckedAnimation: CABasicAnimation = { let unCheck = CABasicAnimation(keyPath: "strokeEnd") unCheck.timingFunction = CAMediaTimingFunction(name: .linear) + unCheck.isRemovedOnCompletion = false unCheck.fillMode = .both - unCheck.duration = 0.33 - unCheck.fromValue = 0 - unCheck.toValue = 1 + unCheck.duration = 0.3 + unCheck.fromValue = 1 + unCheck.toValue = 0 return unCheck }() @@ -84,7 +104,7 @@ import MVMCore /// There is currently no intention on using xib files. required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - fatalError("xib file is not implemented for CheckBox.") + fatalError("xib file is not implemented for Checkbox.") } public convenience init() { @@ -94,8 +114,8 @@ import MVMCore public convenience init(checkedColor: UIColor, unCheckedColor: UIColor, isChecked: Bool = false) { self.init(frame: .zero) isSelected = isChecked - self.checkedColor = checkedColor - self.unCheckedColor = unCheckedColor + self.checkedBackgroundColor = checkedColor + self.unCheckedBackgroundColor = unCheckedColor } //-------------------------------------------------- @@ -109,35 +129,31 @@ import MVMCore public func setupView() { + isUserInteractionEnabled = true translatesAutoresizingMaskIntoConstraints = false backgroundColor = .white - - let path = UIBezierPath() - path.move(to: CGPoint(x: lineWidth / 2, y: bounds.size.height * 0.55)) - path.addLine(to: CGPoint(x: bounds.size.width * 0.45, y: bounds.size.height * 0.85)) - path.addLine(to: CGPoint(x: bounds.size.width - lineWidth / 2, y: lineWidth / 2)) - - let shapeLayer = CAShapeLayer() - self.shapeLayer = shapeLayer - shapeLayer.frame = bounds - layer.addSublayer(shapeLayer) - shapeLayer.strokeColor = lineColor.cgColor - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.path = path.cgPath - shapeLayer.lineJoin = .bevel - shapeLayer.lineWidth = lineWidth + layer.borderWidth = 1 + layer.borderColor = UIColor.black.cgColor } //-------------------------------------------------- - // MARK: - Action + // MARK: - Actions //-------------------------------------------------- open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - print("Action initiated") + super.sendAction(action, to: target, for: event) + + isSelected.toggle() + actionBlock?() + drawCheck() } - + open override func sendActions(for controlEvents: UIControl.Event) { - print("Actions Inititaled") + super.sendActions(for: controlEvents) + + isSelected.toggle() + actionBlock?() + drawCheck() } //-------------------------------------------------- @@ -147,54 +163,62 @@ import MVMCore private func drawCheck() { if shapeLayer == nil { - - layoutIfNeeded() - - let path = UIBezierPath() - path.move(to: CGPoint(x: lineWidth / 2, y: bounds.size.height * 0.55)) - path.addLine(to: CGPoint(x: bounds.size.width * 0.45, y: bounds.size.height * 0.85)) - path.addLine(to: CGPoint(x: bounds.size.width - lineWidth / 2, y: lineWidth / 2)) - - shapeLayer = CAShapeLayer() - shapeLayer?.frame = bounds - layer.addSublayer(shapeLayer!) - shapeLayer?.strokeColor = lineColor.cgColor - shapeLayer?.fillColor = UIColor.clear.cgColor - shapeLayer?.path = path.cgPath - shapeLayer?.lineJoin = .bevel - shapeLayer?.lineWidth = lineWidth - - CATransaction.begin() - CATransaction.setDisableActions(true) - shapeLayer?.strokeEnd = 0.0 - CATransaction.commit() + + let shapeLayer = CAShapeLayer() + self.shapeLayer = shapeLayer + shapeLayer.frame = bounds + layer.addSublayer(shapeLayer) + shapeLayer.strokeColor = lineColor.cgColor + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.path = checkMarkBezierPath.cgPath + shapeLayer.lineJoin = .bevel + shapeLayer.lineWidth = lineWidth + + CATransaction.withDisabledAnimations { + shapeLayer.strokeEnd = 0.0 + } } } /* - //Offsets based on the 124x124 example checkmark - - let startXOffset: Float = 42.0 / 124.0 - let startYOffset: Float = 66.0 / 124.0 - let pivotXOffset: Float = 58.0 / 124.0 - let pivotYOffset: Float = 80.0 / 124.0 - let endXOffset: Float = 83.0 / 124.0 - let endYOffset: Float = 46.0 / 124.0 + // Offsets based on the 124x124 example checkmark + let startXOffset: Float = 42.0 / 124.0 ~~ 0.33871 + let startYOffset: Float = 66.0 / 124.0 ~~ 0.53225 + let pivotXOffset: Float = 58.0 / 124.0 ~~ 0.46774 + let pivotYOffset: Float = 80.0 / 124.0 ~~ 0.64516 + let endXOffset: Float = 83.0 / 124.0 ~~ 0.66935 + let endYOffset: Float = 46.0 / 124.0 ~~ 0.37097 let pivotPercentage: Float = 0.34 let endPercentage = 1.0 - pivotPercentage let animationInterval: Float = 0.01 */ - public func updateCheckboxSelection(_ selected: Bool, animated: Bool) { + /// Returns a UIBezierPath detailing the path of a checkmark + var checkMarkBezierPath: UIBezierPath { - shapeLayer?.removeFromSuperlayer() - shapeLayer = nil + let sideLength = bounds.size.height + let startPoint = CGPoint(x: 0.33871 * sideLength, y: 0.53225 * sideLength) + let pivotOffSet = CGPoint(x: 0.46774 * sideLength, y: 0.64516 * sideLength) + let endOffset = CGPoint(x: 0.66935 * sideLength , y: 0.37097 * sideLength) + + let path = UIBezierPath() + path.move(to: startPoint) + path.addLine(to: pivotOffSet) + path.addLine(to: endOffset) + + return path + } + + public func updateSelection(_ selected: Bool, animated: Bool) { + +// shapeLayer?.removeFromSuperlayer() +// shapeLayer = nil DispatchQueue.main.async { self.isSelected = selected self.drawCheck() - + var layer: CAShapeLayer? if let presentation = self.shapeLayer?.presentation(), animated { layer = presentation @@ -204,24 +228,93 @@ import MVMCore if animated { let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd") + animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear) + animateStrokeEnd.duration = 0.33 animateStrokeEnd.fillMode = .both animateStrokeEnd.isRemovedOnCompletion = false - animateStrokeEnd.duration = 0.33 animateStrokeEnd.fromValue = layer?.strokeEnd ?? 0 animateStrokeEnd.toValue = selected ? 1 : 0 - animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear) layer?.add(animateStrokeEnd, forKey: "strokeEndAnimation") } else { layer?.removeAllAnimations() - CATransaction.begin() - CATransaction.setDisableActions(true) - CATransaction.setAnimationDuration(0) - layer?.strokeEnd = selected ? 1 : 0 - CATransaction.commit() + CATransaction.withDisabledAnimations { + layer?.strokeEnd = selected ? 1 : 0 + } } } } + //-------------------------------------------------- + // MARK: - UITouch + //-------------------------------------------------- + + open override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + if touchIsAcceptablyOutside(touches.first) { + sendActions(for: .touchUpOutside) + } else { + sendActions(for: .touchUpInside) + } + } + + func touchIsAcceptablyOutside(_ touch: UITouch?) -> Bool { + let endLocation = touch?.location(in: self) + let x = endLocation?.x ?? 0.0 + let y = endLocation?.y ?? 0.0 + let faultTolerance: CGFloat = 20.0 + let widthLimit = CGFloat(bounds.size.width + faultTolerance) + let heightLimt = CGFloat(bounds.size.height + faultTolerance) + + return x < -faultTolerance || y < -faultTolerance || x > widthLimit || y > heightLimt + } + + // if checkbox.isSelected { + // UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { + // self.checkbox.backgroundColor = self.checkedColor + // }) + // checkbox.updateCheckSelected(true, animated: animated) + // } else { + // UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { + // self.checkbox.backgroundColor = self.unCheckedColor + // }) + // + // checkbox.updateCheckSelected(false, animated: animated) + // } + + // if delegate && delegate.responds(to: #selector(formValidationProtocol)) && delegate.perform(#selector(formValidationProtocol)).responds(to: #selector(Unmanaged.formValidatorModel)) { + // let formValidator = delegate.perform(#selector(formValidationProtocol)).perform(#selector(Unmanaged.formValidatorModel)) as? FormValidator + // formValidator?.enableByValidation() + // } + + + /* + + - (void)setSelected:(BOOL)selected animated:(BOOL)animated runBlock:(BOOL)runBlock { + [self addAccessibilityLabel:selected]; + + self.isSelected = selected; + if (self.switchSelected && runBlock) { + self.switchSelected(selected); + } + if (selected) { + [UIView animateWithDuration:0.2 delay:0.1 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.checkedSquare.backgroundColor = self.checkedColor; + } completion:nil]; + [self.checkMark updateCheckSelected:YES animated:animated]; + } else { + [UIView animateWithDuration:0.2 delay:0.1 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.checkedSquare.backgroundColor = self.unCheckedColor; + } completion:nil]; + [self.checkMark updateCheckSelected:NO animated:animated]; + } + + if (self.delegate && [self.delegate respondsToSelector:@selector(formValidationProtocol)] && [[self.delegate performSelector:@selector(formValidationProtocol)] respondsToSelector:@selector(formValidatorModel)]) { + FormValidator *formValidator = [[self.delegate performSelector:@selector(formValidationProtocol)] performSelector:@selector(formValidatorModel)]; + [formValidator enableByValidation]; + } + } + */ + //-------------------------------------------------- // MARK: - Molecular //-------------------------------------------------- @@ -239,6 +332,7 @@ import MVMCore } public func updateView(_ size: CGFloat) { + // TODO: Ensure the check logic is resized. } @@ -259,15 +353,27 @@ import MVMCore } if let checkColorHex = dictionary["checkedColor"] as? String { - checkedColor = UIColor.mfGet(forHex: checkColorHex) + checkedBackgroundColor = UIColor.mfGet(forHex: checkColorHex) } if let unCheckedColorHex = dictionary["unCheckedColor"] as? String { - unCheckedColor = UIColor.mfGet(forHex: unCheckedColorHex) + unCheckedBackgroundColor = UIColor.mfGet(forHex: unCheckedColorHex) } - if let backroundColorHex = dictionary["backroundColor"] as? String { - backgroundColor = UIColor.mfGet(forHex: backroundColorHex) - } +// if let actionMap = dictionary.optionalDictionaryForKey("actionMap") { +// actionBlock = { MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) } +// } + } +} + +// TODO: Move to its own extension file. +extension CATransaction { + + /// Performs changes without activating animation actions. + static func withDisabledAnimations(_ actionBlock: ActionBlock) { + CATransaction.begin() + CATransaction.setDisableActions(true) + actionBlock() + CATransaction.commit() } } diff --git a/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift b/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift index 93f8a894..94cd86e2 100644 --- a/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift +++ b/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift @@ -21,16 +21,17 @@ var sizeObject: MFSizeObject? = MFSizeObject(standardSize: Checkbox.defaultHeightWidth, standardiPadPortraitSize: Checkbox.defaultHeightWidth + 6.0) - var checkboxWidthConstraint: NSLayoutConstraint? - var checkboxHeightConstraint: NSLayoutConstraint? - - // A block that is called when the switch is selected. - public var checkboxAction: ((_ selected: Bool) -> ())? - var isRequired = false var fieldKey: String? var delegate: DelegateObject? + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + var checkboxWidthConstraint: NSLayoutConstraint? + var checkboxHeightConstraint: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - Life Cycle //-------------------------------------------------- @@ -43,6 +44,7 @@ translatesAutoresizingMaskIntoConstraints = false addSubview(checkbox) + addSubview(label) let dimension = sizeObject?.getValueBasedOnApplicationWidth() ?? Checkbox.defaultHeightWidth checkboxWidthConstraint = checkbox.heightAnchor.constraint(equalToConstant: dimension) @@ -50,13 +52,15 @@ checkboxHeightConstraint = checkbox.widthAnchor.constraint(equalToConstant: dimension) checkboxHeightConstraint?.isActive = true - NSLayoutConstraint.constraintPinSubview(checkbox, pinTop: true, pinBottom: true, pinLeft: true, pinRight: false) + checkbox.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor).isActive = true + layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: checkbox.bottomAnchor).isActive = true + checkbox.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true checkbox.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - checkbox.lineWidth = 2.0 - addSubview(label) + label.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor).isActive = true + layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor).isActive = true label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true + layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo).isActive = true } @@ -82,89 +86,32 @@ public convenience init(checkedColor: UIColor, unCheckedColor: UIColor, text: String?, isChecked: Bool = false) { self.init(frame: .zero) - checkbox.checkedColor = checkedColor - checkbox.unCheckedColor = unCheckedColor + checkbox.checkedBackgroundColor = checkedColor + checkbox.unCheckedBackgroundColor = unCheckedColor label.text = text } public convenience init(checkedColor: UIColor, unCheck unCheckedColor: UIColor, attributedText: NSAttributedString, isChecked: Bool = false) { self.init(frame: .zero) - checkbox.checkedColor = checkedColor - checkbox.unCheckedColor = unCheckedColor + checkbox.checkedBackgroundColor = checkedColor + checkbox.unCheckedBackgroundColor = unCheckedColor label.attributedText = attributedText } public convenience init(isRoundedCheckbox: Bool) { self.init(frame: .zero) - checkbox.hasRoundCorners = isRoundedCheckbox } - - //-------------------------------------------------- - // MARK: - Methods - //-------------------------------------------------- - - override open class func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { - return CGFloat(Checkbox.defaultHeightWidth) - } - - @objc public func checkboxTapped(checkbox: Checkbox) { -// addAccessibilityLabel(selected) - - checkbox.isSelected.toggle() - - if let checkboxAction = checkboxAction { - checkboxAction(checkbox.isSelected) - } - -// if checkbox.isSelected { -// UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { -// self.checkbox.backgroundColor = self.checkedColor -// }) -// checkbox.updateCheckSelected(true, animated: animated) -// } else { -// UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { -// self.checkbox.backgroundColor = self.unCheckedColor -// }) -// -// checkbox.updateCheckSelected(false, animated: animated) -// } - - // if delegate && delegate.responds(to: #selector(formValidationProtocol)) && delegate.perform(#selector(formValidationProtocol)).responds(to: #selector(Unmanaged.formValidatorModel)) { - // let formValidator = delegate.perform(#selector(formValidationProtocol)).perform(#selector(Unmanaged.formValidatorModel)) as? FormValidator - // formValidator?.enableByValidation() - // } - } - - //-------------------------------------------------- - // MARK: - UITouch - //-------------------------------------------------- - - func touchesEnded(_ touches: Set, with event: UIEvent) { - - if touchIsAcceptablyOutside(touches.first) { - checkbox.sendActions(for: .touchUpOutside) - } else { - checkbox.sendActions(for: .touchUpInside) - } - } - - func touchIsAcceptablyOutside(_ touch: UITouch?) -> Bool { - let endLocation = touch?.location(in: self) - let x = endLocation?.x ?? 0.0 - let y = endLocation?.y ?? 0.0 - let faultTolerance: CGFloat = 20.0 - let widthLimit = CGFloat(bounds.size.width + faultTolerance) - let heightLimt = CGFloat(bounds.size.height + faultTolerance) - - return x < -faultTolerance || y < -faultTolerance || x > widthLimit || y > heightLimt - } } // MARK: - Molecular extension CheckboxWithLabelView { + override open class func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { + return CGFloat(Checkbox.defaultHeightWidth) + } + @objc override open func updateView(_ size: CGFloat) { DispatchQueue.main.async { @@ -198,8 +145,8 @@ extension CheckboxWithLabelView { self.isRequired = isRequired } - checkbox.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) - label.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + checkbox.setWithJSON(dictionary.dictionaryForKey("checkbox"), delegateObject: delegateObject, additionalData: additionalData) + label.setWithJSON(dictionary.dictionaryForKey("label"), delegateObject: delegateObject, additionalData: additionalData) } }