From 77a3eb882781d9814e1268d9f1ecce85152ee6d1 Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Wed, 2 Oct 2019 15:22:12 -0400 Subject: [PATCH] Added a new extension file for CATransaction. Implementation of Checkbox and CheckboxWithLabel. --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + MVMCoreUI/Atoms/Views/Checkbox.swift | 323 +++++++++--------- .../Atoms/Views/CheckboxWithLabelView.swift | 204 +++++++---- .../Utility/CATransaction+Extension.swift | 21 ++ 4 files changed, 321 insertions(+), 231 deletions(-) create mode 100644 MVMCoreUI/Utility/CATransaction+Extension.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 9692652c..253371bc 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 01E569D3223FFFA500327251 /* ThreeLayerViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = D2A5146A2214905000345BFB /* ThreeLayerViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */; }; 0A1B4A96233BB18F005B3FB4 /* CheckboxWithLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */; }; + 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; }; 948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948DB67D2326DCD90011F916 /* MultiProgress.swift */; }; B8200E152280C4CF007245F4 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E142280C4CF007245F4 /* ProgressBar.swift */; }; @@ -205,6 +206,7 @@ 01DF55DF21F8FAA800CC099B /* MFTextFieldListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MFTextFieldListView.swift; sourceTree = ""; }; 01DF566F21FA5AB300CC099B /* TextFieldListFormViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldListFormViewController.swift; sourceTree = ""; }; 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionDetailWithImage.swift; sourceTree = ""; }; + 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = ""; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxWithLabelView.swift; sourceTree = ""; }; 948DB67D2326DCD90011F916 /* MultiProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgress.swift; sourceTree = ""; }; @@ -654,6 +656,7 @@ D29DF2A021E7AF4E003B2FB9 /* MVMCoreUIUtility.m */, D29DF2A721E7B2F9003B2FB9 /* MVMCoreUIConstants.h */, D29DF2A821E7B2F9003B2FB9 /* MVMCoreUIConstants.m */, + 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */, ); path = Utility; sourceTree = ""; @@ -1095,6 +1098,7 @@ D29DF25121E6A177003B2FB9 /* MFDigitTextBox.m in Sources */, DBC4391B224421A0001AB423 /* CaretButton.swift in Sources */, 0198F7A82256A80B0066C936 /* MFRadioButton.m in Sources */, + 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */, D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */, D29DF29C21E7ADB9003B2FB9 /* MFProgrammaticTableViewController.m in Sources */, 0105618E224BBE7700E1557D /* FormValidator+TextFields.swift in Sources */, diff --git a/MVMCoreUI/Atoms/Views/Checkbox.swift b/MVMCoreUI/Atoms/Views/Checkbox.swift index d0599043..9fac4910 100644 --- a/MVMCoreUI/Atoms/Views/Checkbox.swift +++ b/MVMCoreUI/Atoms/Views/Checkbox.swift @@ -8,6 +8,22 @@ import MVMCore +/* + !!! -- DO NOT REMOVE -- !!! + (Unless Design changes the appearance of the checkmark). + + // 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 + */ + /** This class expects its height and width to be equal. */ @@ -16,85 +32,82 @@ import MVMCore // MARK: - Properties //-------------------------------------------------- + // Form Validation + var isRequired = false + var fieldKey: String? + var delegateObject: DelegateObject? + public static let defaultHeightWidth: CGFloat = 18.0 - /// The color of the background when checked. - public var checkedBackgroundColor: UIColor? + /// If true the border of this checkbox will be circular. + public var isRound: Bool = false - /// The color of the background when unChecked. Will change of the view's background color. - public var unCheckedBackgroundColor: UIColor = .white { - didSet (newColor) { - backgroundColor = newColor + /// 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 = .white { + didSet { + if isSelected { + backgroundColor = checkedBackgroundColor + } } } - /// If true the border of this checkbox will be circular. - public var hasRoundedCorners = 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? + /// The color of the background when unChecked. + public var unCheckedBackgroundColor: UIColor = .white { + didSet { + if !isSelected { + backgroundColor = unCheckedBackgroundColor + } + } + } public var cornerRadiusValue: CGFloat { return bounds.size.height / 2 } - public var lineWidth: CGFloat = 2 - public var lineColor: UIColor = .black - public var borderColor: UIColor = .black + /// Action Block called when the switch is selected. + public var actionBlock: ActionBlock? + + /// Manages the appearance of the checkbox. + private var shapeLayer: CAShapeLayer? + + public var checkWidth: CGFloat = 2 + public var checkColor: UIColor = .black public var borderWidth: CGFloat = 1 + public var borderColor: UIColor = .black override open var isSelected: Bool { didSet { - shapeLayer?.removeAllAnimations() - if isSelected { - if checkedBackgroundColor != nil { - UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { - self.backgroundColor = self.checkedBackgroundColor - }) + if !updateSelectionOnly { + layoutIfNeeded() + shapeLayer?.removeAllAnimations() + + updateCheckboxUI(selection: isSelected, isAnimated: isAnimated) + + if let delegate = delegateObject as? FormValidationProtocol { + delegate.formValidatorModel?()?.enableByValidation() } - shapeLayer?.add(checkedAnimation, forKey: "strokeEnd") - } else { - if checkedBackgroundColor != nil { - UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { - self.backgroundColor = self.unCheckedBackgroundColor - }) - } - shapeLayer?.add(uncheckedAnimation, forKey: "strokeEnd") + + accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state") } } } - lazy private var checkedAnimation: CABasicAnimation = { - let check = CABasicAnimation(keyPath: "strokeEnd") - check.timingFunction = CAMediaTimingFunction(name: .linear) - check.isRemovedOnCompletion = false - check.fillMode = .both - check.duration = 0.3 - check.fromValue = 0 - check.toValue = 1 - return check - }() - - lazy private var uncheckedAnimation: CABasicAnimation = { - let unCheck = CABasicAnimation(keyPath: "strokeEnd") - unCheck.timingFunction = CAMediaTimingFunction(name: .linear) - unCheck.isRemovedOnCompletion = false - unCheck.fillMode = .both - unCheck.duration = 0.3 - unCheck.fromValue = 1 - unCheck.toValue = 0 - return unCheck - }() - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- override public init(frame: CGRect) { super.init(frame: frame) + + accessibilityTraits = .none + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint") + setupView() } @@ -108,16 +121,20 @@ import MVMCore self.init(frame:.zero) } - public convenience init(checkedColor: UIColor, unCheckedColor: UIColor, isChecked: Bool = false) { + public convenience init(isChecked: Bool) { self.init(frame: .zero) + updateSelectionOnly = true isSelected = isChecked - self.checkedBackgroundColor = checkedColor - self.unCheckedBackgroundColor = unCheckedColor + updateSelectionOnly = false } - public convenience init(isCircular: Bool) { + public convenience init(checkedBackgroundColor: UIColor, unCheckedBackgroundColor: UIColor, isChecked: Bool = false) { self.init(frame: .zero) - hasRoundedCorners = isCircular + updateSelectionOnly = true + isSelected = isChecked + updateSelectionOnly = false + self.checkedBackgroundColor = checkedBackgroundColor + self.unCheckedBackgroundColor = unCheckedBackgroundColor } //-------------------------------------------------- @@ -126,9 +143,9 @@ import MVMCore override open func layoutSubviews() { super.layoutSubviews() + drawCheck() - layer.cornerRadius = hasRoundedCorners ? cornerRadiusValue : 0 - backgroundColor = unCheckedBackgroundColor + layer.cornerRadius = isRound ? cornerRadiusValue : 0 layer.borderWidth = borderWidth layer.borderColor = borderColor.cgColor } @@ -137,6 +154,7 @@ import MVMCore isUserInteractionEnabled = true translatesAutoresizingMaskIntoConstraints = false + backgroundColor = .white } //-------------------------------------------------- @@ -161,6 +179,7 @@ import MVMCore // MARK: - Methods //-------------------------------------------------- + /// Creates the check mark used for the checkbox. private func drawCheck() { if shapeLayer == nil { @@ -169,36 +188,20 @@ import MVMCore self.shapeLayer = shapeLayer shapeLayer.frame = bounds layer.addSublayer(shapeLayer) - shapeLayer.strokeColor = lineColor.cgColor + shapeLayer.strokeColor = checkColor.cgColor shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.path = checkMarkBezierPath.cgPath + shapeLayer.path = checkMarkPath.cgPath shapeLayer.lineJoin = .bevel - shapeLayer.lineWidth = lineWidth + shapeLayer.lineWidth = checkWidth CATransaction.withDisabledAnimations { - shapeLayer.strokeEnd = 0.0 + shapeLayer.strokeEnd = isSelected ? 1 : 0 } } } - /* - !!! -- DO NOT REMOVE -- !!! - (Unless Design changes the appearance of the checkmark). - - // 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 - */ - /// Returns a UIBezierPath detailing the path of a checkmark - var checkMarkBezierPath: UIBezierPath { + var checkMarkPath: UIBezierPath { let sideLength = bounds.size.height let startPoint = CGPoint(x: 0.33871 * sideLength, y: 0.53225 * sideLength) @@ -213,38 +216,43 @@ import MVMCore return path } - public func updateSelection(_ selected: Bool, animated: Bool) { - - // shapeLayer?.removeFromSuperlayer() - // shapeLayer = nil + /// 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.updateSelectionOnly = true self.isSelected = selected + self.updateSelectionOnly = false self.drawCheck() + self.shapeLayer?.removeAllAnimations() + self.updateCheckboxUI(selection: selected, isAnimated: animated) + } + } + + func updateCheckboxUI(selection: 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 = !selection ? 1 : 0 + animateStrokeEnd.toValue = selection ? 1 : 0 + self.shapeLayer?.add(animateStrokeEnd, forKey: "strokeEnd") - var layer: CAShapeLayer? - if let presentation = self.shapeLayer?.presentation(), animated { - layer = presentation - } else { - layer = self.shapeLayer + UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: { + self.backgroundColor = selection ? self.checkedBackgroundColor : self.unCheckedBackgroundColor + }) + } else { + CATransaction.withDisabledAnimations { + self.shapeLayer?.strokeEnd = selection ? 1 : 0 } - if animated { - let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd") - animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear) - animateStrokeEnd.duration = 0.33 - animateStrokeEnd.fillMode = .both - animateStrokeEnd.isRemovedOnCompletion = false - animateStrokeEnd.fromValue = layer?.strokeEnd ?? 0 - animateStrokeEnd.toValue = selected ? 1 : 0 - layer?.add(animateStrokeEnd, forKey: "strokeEndAnimation") - } else { - layer?.removeAllAnimations() - CATransaction.withDisabledAnimations { - layer?.strokeEnd = selected ? 1 : 0 - } - } + self.backgroundColor = selection ? self.checkedBackgroundColor : self.unCheckedBackgroundColor } } @@ -254,11 +262,7 @@ import MVMCore open override func touchesEnded(_ touches: Set, with event: UIEvent?) { - if touchIsAcceptablyOutside(touches.first) { - sendActions(for: .touchUpOutside) - } else { - sendActions(for: .touchUpInside) - } + sendActions(for: touchIsAcceptablyOutside(touches.first) ? .touchUpOutside : .touchUpInside) } func touchIsAcceptablyOutside(_ touch: UITouch?) -> Bool { @@ -272,40 +276,6 @@ import MVMCore return x < -faultTolerance || y < -faultTolerance || x > widthLimit || y > heightLimt } - // 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 //-------------------------------------------------- @@ -322,15 +292,22 @@ import MVMCore setupView() } - public func updateView(_ size: CGFloat) { - - // TODO: Ensure the check logic is resized. - } + public func updateView(_ size: CGFloat) { } public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + self.delegateObject = delegateObject + FormValidator.setupValidation(molecule: self, delegate: delegateObject?.formValidationProtocol) guard let dictionary = json else { return } + if let fieldKey = dictionary[KeyFieldKey] as? String { + self.fieldKey = fieldKey + } + + if let isRequired = dictionary[KeyRequired] as? Bool { + self.isRequired = isRequired + } + if let borderColorHex = dictionary["borderColor"] as? String { layer.borderColor = UIColor.mfGet(forHex: borderColorHex).cgColor } @@ -339,36 +316,50 @@ import MVMCore layer.borderWidth = borderWidth } - if let checkColorHex = dictionary["lineColor"] as? String { - lineColor = UIColor.mfGet(forHex: checkColorHex) + if let isChecked = dictionary["isChecked"] as? Bool, isChecked { + updateSelectionOnly = true + isSelected = isChecked + updateSelectionOnly = false } - if let checkColorHex = dictionary["checkedColor"] as? String { - checkedBackgroundColor = UIColor.mfGet(forHex: checkColorHex) + if let checkColorHex = dictionary["checkColor"] as? String { + checkColor = UIColor.mfGet(forHex: checkColorHex) } - if let unCheckedColorHex = dictionary["unCheckedColor"] as? String { - unCheckedBackgroundColor = UIColor.mfGet(forHex: unCheckedColorHex) + if let unCheckedBackgroundColorHex = dictionary["unCheckedBackgroundColor"] as? String { + unCheckedBackgroundColor = UIColor.mfGet(forHex: unCheckedBackgroundColorHex) + } + + if let checkedBackgroundColorHex = dictionary["checkedBackgroundColor"] as? String { + checkedBackgroundColor = UIColor.mfGet(forHex: checkedBackgroundColorHex) + } + + if let isAnimated = dictionary["isAnimated"] as? Bool { + self.isAnimated = isAnimated } if let isRound = dictionary["isRound"] as? Bool { - hasRoundedCorners = isRound + self.isRound = isRound } - // if let actionMap = dictionary.optionalDictionaryForKey("actionMap") { - // actionBlock = { MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) } - // } + 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 { +// MARK:- FormValidationProtocol +extension Checkbox: FormValidationProtocol { - /// Performs changes without activating animation actions. - static func withDisabledAnimations(_ actionBlock: ActionBlock) { - CATransaction.begin() - CATransaction.setDisableActions(true) - actionBlock() - CATransaction.commit() + public func isValidField() -> Bool { + return isRequired ? isSelected : true + } + + public func formFieldName() -> String? { + return fieldKey + } + + public func formFieldValue() -> Any? { + return NSNumber(value: isSelected) } } diff --git a/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift b/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift index db551800..bf0288ae 100644 --- a/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift +++ b/MVMCoreUI/Atoms/Views/CheckboxWithLabelView.swift @@ -12,8 +12,8 @@ // MARK: - Outlets //-------------------------------------------------- - let checkbox = Checkbox() - let label = Label.commonLabelB2(true) + public let checkbox = Checkbox() + public let label = Label.commonLabelB2(true) //-------------------------------------------------- // MARK: - Properties @@ -21,17 +21,34 @@ var sizeObject: MFSizeObject? = MFSizeObject(standardSize: Checkbox.defaultHeightWidth, standardiPadPortraitSize: Checkbox.defaultHeightWidth + 6.0) - var isRequired = false - var fieldKey: String? - var delegate: DelegateObject? + var checkboxPosition: CheckboxPosition = .centerLeft + + public enum CheckboxPosition: String { + case centerLeft + case topLeft + case bottomLeft + } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- - + var checkboxWidthConstraint: NSLayoutConstraint? var checkboxHeightConstraint: NSLayoutConstraint? + var checkboxLeadingConstraint: NSLayoutConstraint? + var checkboxTrailingConstraint: NSLayoutConstraint? + var checkboxTopConstraint: NSLayoutConstraint? + var checkboxBottomConstraint: NSLayoutConstraint? + var checkboxCenterYConstraint: NSLayoutConstraint? + + var checkboxLabelConstraint: NSLayoutConstraint? + + var labelLeadingConstraint: NSLayoutConstraint? + var labelTrailingConstraint: NSLayoutConstraint? + var labelTopConstraint: NSLayoutConstraint? + var labelBottomConstraint: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - Life Cycle //-------------------------------------------------- @@ -46,23 +63,24 @@ addSubview(checkbox) addSubview(label) + label.setContentCompressionResistancePriority(.required, for: .vertical) + let dimension = sizeObject?.getValueBasedOnApplicationWidth() ?? Checkbox.defaultHeightWidth checkboxWidthConstraint = checkbox.heightAnchor.constraint(equalToConstant: dimension) checkboxWidthConstraint?.isActive = true checkboxHeightConstraint = checkbox.widthAnchor.constraint(equalToConstant: dimension) checkboxHeightConstraint?.isActive = true - 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.checkedBackgroundColor = .yellow + alignSubviews(by: .centerLeft) - label.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor).isActive = true - layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor).isActive = true - label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true - label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo).isActive = true + label.isUserInteractionEnabled = true + let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) + tap.numberOfTapsRequired = 1 + label.addGestureRecognizer(tap) + } + + @objc func tapped() { + checkbox.updateSelection(to: !checkbox.isSelected, animated: true) } //-------------------------------------------------- @@ -77,13 +95,18 @@ override public init(frame: CGRect) { super.init(frame: frame) setupView() -// addAccessibleProperties() } public convenience init() { self.init(frame: .zero) } + public convenience init(position: CheckboxPosition) { + self.init(frame: .zero) + + alignSubviews(by: position) + } + public convenience init(checkedColor: UIColor, unCheckedColor: UIColor, text: String?, isChecked: Bool = false) { self.init(frame: .zero) @@ -99,79 +122,130 @@ checkbox.unCheckedBackgroundColor = unCheckedColor label.attributedText = attributedText } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// Aligns Checkbox and Label relative to the desired position of the Checkbox. + private func alignSubviews(by position: CheckboxPosition) { + checkboxPosition = position + + label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true + + switch position { + case .centerLeft: + checkboxTopConstraint = checkbox.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor) + checkboxBottomConstraint = layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: checkbox.bottomAnchor) + checkboxLeadingConstraint = checkbox.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + checkboxCenterYConstraint = checkbox.centerYAnchor.constraint(equalTo: centerYAnchor) + + labelTopConstraint = label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor) + labelTrailingConstraint = layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor) + checkboxLabelConstraint = label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo) + + case .topLeft: + checkboxTopConstraint = checkbox.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor) + checkboxBottomConstraint = layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: checkbox.bottomAnchor) + checkboxLeadingConstraint = checkbox.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + + labelTopConstraint = label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor) + labelTrailingConstraint = layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor) + checkboxLabelConstraint = label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo) + + case .bottomLeft: + checkboxTopConstraint = checkbox.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor) + checkboxBottomConstraint = layoutMarginsGuide.bottomAnchor.constraint(equalTo: checkbox.bottomAnchor) + checkboxLeadingConstraint = checkbox.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + + labelTopConstraint = label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor) + labelTrailingConstraint = layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor) + checkboxLabelConstraint = label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo) + labelBottomConstraint = layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor) + } + + if position != .bottomLeft { + layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor).isActive = true + let labelBottom = label.bottomAnchor.constraint(equalTo: bottomAnchor) + labelBottomConstraint = labelBottom + labelBottom.priority = UILayoutPriority(249) + labelBottom.isActive = true + } + + toggleSubviewConstraints(isActive: true) + } + + func toggleSubviewConstraints(isActive state: Bool) { + + checkboxLeadingConstraint?.isActive = state + checkboxTrailingConstraint?.isActive = state + checkboxTopConstraint?.isActive = state + checkboxBottomConstraint?.isActive = state + checkboxCenterYConstraint?.isActive = state + labelLeadingConstraint?.isActive = state + labelTrailingConstraint?.isActive = state + labelTopConstraint?.isActive = state + labelBottomConstraint?.isActive = state + checkboxLabelConstraint?.isActive = state + + if !state { + checkboxLeadingConstraint = nil + checkboxTrailingConstraint = nil + checkboxTopConstraint = nil + checkboxBottomConstraint = nil + checkboxCenterYConstraint = nil + labelLeadingConstraint = nil + labelTrailingConstraint = nil + labelTopConstraint = nil + labelBottomConstraint = nil + checkboxLabelConstraint = nil + } + } } -// MARK: - Molecular +/// MARK: - Molecular extension CheckboxWithLabelView { - override open class func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { + 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 { - self.label.updateView(size) - if self.checkbox.responds(to: #selector(self.updateView(_:))) { - if let dimension = self.sizeObject?.getValueBased(onSize: size) { - self.checkboxWidthConstraint?.constant = dimension - self.checkboxHeightConstraint?.constant = dimension - self.checkbox.updateView(size) - } + label.updateView(size) + + if self.checkbox.responds(to: #selector(self.updateView(_:))) { + if let dimension = sizeObject?.getValueBased(onSize: size) { + checkboxWidthConstraint?.constant = dimension + checkboxHeightConstraint?.constant = dimension + checkbox.updateView(size) } } + + layoutIfNeeded() } override open func alignment() -> UIStackView.Alignment { return .leading } + open override func resetConstraints() { + super.resetConstraints() + + toggleSubviewConstraints(isActive: false) + } + override open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) - delegate = delegateObject - FormValidator.setupValidation(molecule: self, delegate: delegateObject?.formValidationProtocol) guard let dictionary = json else { return } - if let fieldKey = dictionary[KeyFieldKey] as? String { - self.fieldKey = fieldKey - } - - if let isRequired = dictionary[KeyRequired] as? Bool { - self.isRequired = isRequired + if let checkboxAlignment = dictionary["checkboxAlignment"] as? String, let position = CheckboxPosition(rawValue: checkboxAlignment) { + toggleSubviewConstraints(isActive: false) + alignSubviews(by: position) } checkbox.setWithJSON(dictionary.dictionaryForKey("checkbox"), delegateObject: delegateObject, additionalData: additionalData) label.setWithJSON(dictionary.dictionaryForKey("label"), delegateObject: delegateObject, additionalData: additionalData) } } - -// MARK:- FormValidationProtocol -extension CheckboxWithLabelView: FormValidationProtocol { - - public func isValidField() -> Bool { - return isRequired ? checkbox.isSelected : true - } - - public func formFieldName() -> String? { - return fieldKey - } - - public func formFieldValue() -> Any? { - return NSNumber(value: checkbox.isSelected) - } -} - -// MARK:- Accessibility -extension CheckboxWithLabelView { - - func addAccessibleProperties() { -// accessibilityTraits = .none -// accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint") - } - - func addAccessibilityLabel(_ selected: Bool) { -// let state = selected ? MVMCoreUIUtility.hardcodedString(withKey: "checkbox_checked_state") : MVMCoreUIUtility.hardcodedString(withKey: "checkbox_unchecked_state") -// accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state"), label.text ?? "", state) - } -} diff --git a/MVMCoreUI/Utility/CATransaction+Extension.swift b/MVMCoreUI/Utility/CATransaction+Extension.swift new file mode 100644 index 00000000..66bb33d3 --- /dev/null +++ b/MVMCoreUI/Utility/CATransaction+Extension.swift @@ -0,0 +1,21 @@ +// +// CATransaction+Extension.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 10/2/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import Foundation + + +extension CATransaction { + + /// Performs changes without activating animation actions. + static func withDisabledAnimations(_ actionBlock: ActionBlock) { + CATransaction.begin() + CATransaction.setDisableActions(true) + actionBlock() + CATransaction.commit() + } +}