Decent working state with animation and touch.

This commit is contained in:
Kevin G Christiano 2019-09-27 13:43:02 -04:00
parent afcf8d535b
commit f5f994648a
2 changed files with 212 additions and 159 deletions

View File

@ -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<UITouch>, 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<AnyObject>.formValidatorModel)) {
// let formValidator = delegate.perform(#selector(formValidationProtocol)).perform(#selector(Unmanaged<AnyObject>.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()
}
}

View File

@ -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<AnyObject>.formValidatorModel)) {
// let formValidator = delegate.perform(#selector(formValidationProtocol)).perform(#selector(Unmanaged<AnyObject>.formValidatorModel)) as? FormValidator
// formValidator?.enableByValidation()
// }
}
//--------------------------------------------------
// MARK: - UITouch
//--------------------------------------------------
func touchesEnded(_ touches: Set<UITouch>, 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)
}
}