diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 900edec2..33529648 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */; }; 0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; }; + 0AA33B3A2398524F0067DD0F /* Switch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA33B392398524F0067DD0F /* Switch.swift */; }; 943784F5236B77BB006A1E82 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F3236B77BB006A1E82 /* GraphView.swift */; }; 943784F6236B77BB006A1E82 /* GraphViewAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */; }; 9455B19C234F8A0400A574DB /* MVMAnimationFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */; }; @@ -226,6 +227,7 @@ 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.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 = ""; }; + 0AA33B392398524F0067DD0F /* Switch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Switch.swift; sourceTree = ""; }; 943784F3236B77BB006A1E82 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphViewAnimationHandler.swift; sourceTree = ""; }; 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MVMAnimationFramework.framework; path = ../SharedFrameworks/MVMAnimationFramework.framework; sourceTree = ""; }; @@ -779,6 +781,7 @@ 01004F2F22721C3800991ECC /* RadioButton.swift */, 943784F3236B77BB006A1E82 /* GraphView.swift */, 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */, + 0AA33B392398524F0067DD0F /* Switch.swift */, ); path = Views; sourceTree = ""; @@ -1082,6 +1085,7 @@ D29DF11721E6805F003B2FB9 /* UIColor+MFConvenience.m in Sources */, D29DF25321E6A177003B2FB9 /* MFDigitTextField.m in Sources */, D2B18B7F2360913400A9AEDC /* Control.swift in Sources */, + 0AA33B3A2398524F0067DD0F /* Switch.swift in Sources */, D29DF12F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m in Sources */, DBC4392122491730001AB423 /* LabelWithInternalButton.swift in Sources */, D224798C231450C8003FCCF9 /* HeadlineBodySwitch.swift in Sources */, diff --git a/MVMCoreUI/Atoms/Views/Switch.swift b/MVMCoreUI/Atoms/Views/Switch.swift new file mode 100644 index 00000000..d7e9b25e --- /dev/null +++ b/MVMCoreUI/Atoms/Views/Switch.swift @@ -0,0 +1,376 @@ +// +// Switch.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 12/4/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import MVMCore +import MVMCoreUI +import UIKit + +typealias ValueChangeBlock = () -> Void + +open class Switch: UIControl, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, FormValidationFormFieldProtocol { + var on = false + var onTintColor: UIColor? + var offTintColor: UIColor? + var onKnobTintColor: UIColor? + var offKnobTintColor: UIColor? + var shouldTouchToSwitch = false + var valueChangedBlock: ValueChangeBlock? + var actionBlock: ValueChangeBlock? + + let SwitchWidth: CGFloat = 42 + let SwitchHeight: CGFloat = 22 + let SwitchKnobWidth: CGFloat = 20 + let SwitchKnobHeight: CGFloat = 20 + let SwitchShakeIntensity: CGFloat = 2 + + private weak var baseView: UIView? + private weak var knobView: UIView? + private var knobLeftConstraint: NSLayoutConstraint? + private var knobRightConstraint: NSLayoutConstraint? + private var knobHeightConstraint: NSLayoutConstraint? + private var knobWidthConstraint: NSLayoutConstraint? + private weak var height: NSLayoutConstraint? + private weak var width: NSLayoutConstraint? + private var valueShouldChange = false + private var canChangeValue = false + private var json: [AnyHashable: Any]? + private var delegate: DelegateObject? + + public func updateView(_ size: CGFloat) { + height?.constant = MVMCoreUISwitch.getHeight() + width?.constant = MVMCoreUISwitch.getWidth() + baseView?.layer.cornerRadius = MVMCoreUISwitch.getHeight() / 2.0 + knobView?.layer.cornerRadius = MVMCoreUISwitch.getKnobHeight() * 0.5 + knobHeightConstraint?.constant = MVMCoreUISwitch.getKnobHeight() + knobWidthConstraint?.constant = MVMCoreUISwitch.getKnobWidth() + } + + public func setupView() { + onTintColor = UIColor.mfSwitchOnTint() + offTintColor = UIColor.mfSwitchOffTint() + canChangeValue = true + shouldTouchToSwitch = true + setUpViewWithState(on) + accessibilityLabel() = MVMCoreUIUtility.hardcodedString(withKey: "MVMCoreUISwitch_buttonlabel") + } + + public required init?(coder: NSCoder) { + if super.init(coder: coder) != nil { + setupView() + } + } + + public init(frame: CGRect) { + if super.init(frame: frame) != nil { + setupView() + } + } + + public init() { + if super.init() { + setupView() + } + } + + public class func mvmSwitchDefault() -> Self { + let mySwitch = self.init(frame: CGRect.zero) + mySwitch?.translatesAutoresizingMaskIntoConstraints = false + return mySwitch + } + + public class func mvmSwitchDefault(with block: ValueChangeBlock?) -> Self { + let mySwitch = self.init(frame: CGRect.zero) + mySwitch?.valueChangedBlock = block + return mySwitch + } + + public func setUpViewWithState(_ on: Bool) { + if !self.baseView { + isUserInteractionEnabled = true + valueShouldChange = true + + let baseView = MVMCoreUICommonViewsUtility.commonView() + if let baseView = baseView { + addSubview(baseView) + } + NSLayoutConstraint.constraintPinSubview(toSuperview: baseView) + let constraints = NSLayoutConstraint.constraintPinView(baseView, heightConstraint: true, heightConstant: MVMCoreUISwitch.getHeight(), widthConstraint: true, widthConstant: MVMCoreUISwitch.getWidth()) + self.height = constraints.object(forKey: ConstraintHeight, ofType: NSLayoutConstraint.self) + self.width = constraints.object(forKey: ConstraintWidth, ofType: NSLayoutConstraint.self) + baseView?.layer.cornerRadius = MVMCoreUISwitch.getHeight() / 2.0 + + onKnobTintColor = UIColor.white + offKnobTintColor = UIColor.white + + let knobView = MVMCoreUICommonViewsUtility.commonView() + knobView?.backgroundColor = UIColor.white + knobView?.layer.cornerRadius = MVMCoreUISwitch.getKnobHeight() * 0.5 + if let knobView = knobView { + baseView?.addSubview(knobView) + } + let heightWidth = NSLayoutConstraint.constraintPinView(knobView, heightConstraint: true, heightConstant: MVMCoreUISwitch.getKnobHeight(), widthConstraint: true, widthConstant: MVMCoreUISwitch.getKnobWidth()) + let height = heightWidth.object(forKey: ConstraintHeight, ofType: NSLayoutConstraint.self) + let width = heightWidth.object(forKey: ConstraintWidth, ofType: NSLayoutConstraint.self) + knobHeightConstraint = height + knobWidthConstraint = width + let leadingTrailingDic = NSLayoutConstraint.constraintPinSubview(knobView, pinTop: false, topConstant: 0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: 1, pinRight: true, rightConstant: 1) + + let leadingTrailingDic = NSLayoutConstraint.constraintPinSubview(knobView, pinTop: false, topConstant: 0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: 1, pinRight: true, rightConstant: 1) + let `left` = leadingTrailingDic.object(forKey: ConstraintLeading, ofType: NSLayoutConstraint.self) + let `right` = leadingTrailingDic.object(forKey: ConstraintTrailing, ofType: NSLayoutConstraint.self) + NSLayoutConstraint.constraintPinSubview(knobView, pinCenterX: false, centerXConstant: 0, pinCenterY: true, centerYConstant: 0) + `right`?.constant = 15 + knobLeftConstraint = `left` + knobRightConstraint = `right` + baseView.bringSubviewToFront(knobView) + + //baseView = baseView // Skipping redundant initializing to itself + //knobView = knobView // Skipping redundant initializing to itself + + baseView.isUserInteractionEnabled = false + knobView.isUserInteractionEnabled = false + shouldTouchToSwitch = false + setState(on, animated: false) + shouldTouchToSwitch = true + } + + // MARK: - MVMCoreUIMoleculeViewProtocol + public func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { + self.json = json + delegate = delegateObject + + FormValidator.setupValidation(withMolecule: self, delegate: delegateObject?.formValidationProtocol) + + var color = json?.string("onTintColor") + if color != nil { + onTintColor = UIColor.mfGet(forHex: color) + } + + color = json?.string("offTintColor") + if color != nil { + offTintColor = UIColor.mfGet(forHex: color) + } + + color = json?.string("onKnobTintColor") + if color != nil { + onKnobTintColor = UIColor.mfGet(forHex: color) + } + + color = json?.string("offKnobTintColor") + if color != nil { + offKnobTintColor = UIColor.mfGet(forHex: color) + } + + setState(json?.bool(forKey: "state"), animated: false) + + let actionMap = json?.dict("actionMap") + if actionMap != nil { + addTarget(self, action: #selector(addCustomAction), for: .touchUpInside) + } + } + + public @objc func addCustomAction() { + if actionBlock { + actionBlock() + } + } + + public class func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { + return self.getSwitchHeight() + } + + // MARK: - MVMCoreUIMoleculeViewProtocol + public func needsToBeConstrained() -> Bool { + return true + } + + public func alignment() -> UIStackView.Alignment { + return .trailing + } + + // MARK: - UIResponder overide + public func touchesBegan(_ touches: Set, with event: UIEvent) { + knobEnlargeAnimation() + sendActions(for: .touchDown) + } + + public func touchesEnded(_ touches: Set, with event: UIEvent) { + if shouldTouchToSwitch { + if valueShouldChange { + changeValue() + } + } + sendActions(for: .touchUpInside) + knobMoveAnitmationTo(on: isOn) + knobShakeAnitmationTo(on: isOn) + knobReformAnimation(true) + valueShouldChange = true + } + + public func touchesMoved(_ touches: Set, with event: UIEvent) { + if shouldTouchToSwitch { + if touchMoves(toLeft: touches) { + knobMoveAnitmationTo(on: false) + knobShakeAnitmationTo(on: false) + knobReformAnimation(true) + } else { + knobMoveAnitmationTo(on: true) + knobShakeAnitmationTo(on: true) + knobReformAnimation(true) + } + } + if touchIsOutSide(touches) { + sendActions(for: .touchDragOutside) + if !shouldTouchToSwitch { + knobReformAnimation(true) + } + } else { + sendActions(for: .touchDragInside) + } + } + + public func touchesCancelled(_ touches: Set, with event: UIEvent) { + if shouldTouchToSwitch { + knobReformAnimation(true) + canChangeValue = true + } + sendActions(for: .touchCancel) + } + + // MARK: - animation + public func knobEnlargeAnimation() { + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint.constant += PaddingOne + self.layoutIfNeeded() + }) + } + + public func knobReformAnimation(_ animated: Bool) { + if animated { + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint.constant = MVMCoreUISwitch.getKnobWidth() + self.layoutIfNeeded() + }) { finished in + } + } else { + knobWidthConstraint.constant = MVMCoreUISwitch.getKnobWidth() + layoutIfNeeded() + } + } + + public func knobMoveAnitmationTo(on toOn: Bool) { + UIView.animate(withDuration: 0.1, animations: { + if toOn { + self.knobLeftConstraint.priority = 1 + self.knobRightConstraint.priority = 999 + self.knobRightConstraint.constant = 1 + } else { + self.knobLeftConstraint.priority = 999 + self.knobRightConstraint.priority = 1 + self.knobLeftConstraint.constant = 1 + } + self.setBaseColorToOn(toOn, animated: true) + self.knobWidthConstraint.constant = MVMCoreUISwitch.getKnobWidth() + PaddingOne + self.valueShouldChange = toOn != self.on + self.layoutIfNeeded() + }) + } + + public func setBaseColorToOn(_ toOn: Bool, animated: Bool) { + if animated { + UIView.beginAnimations(nil, context: nil) + UIView.setAnimationDuration(0.2) + UIView.setAnimationCurve(.easeIn) + if toOn { + knobView.backgroundColor = onKnobTintColor + baseView.backgroundColor = onTintColor + } else { + knobView.backgroundColor = offKnobTintColor + baseView.backgroundColor = offTintColor + } + UIView.commitAnimations() + } else if on { + knobView.backgroundColor = onKnobTintColor + baseView.backgroundColor = onTintColor + } else { + knobView.backgroundColor = offKnobTintColor + baseView.backgroundColor = offTintColor + } + } + + //used after knob moving to match the behavior of default uiswitch + public func knobShakeAnitmationTo(on toOn: Bool) { + UIView.animate(withDuration: 0.1, delay: 0.1, options: .curveEaseIn, animations: { + if toOn { + self.knobRightConstraint.constant = SwitchShakeIntensity + } else { + self.knobLeftConstraint.constant = SwitchShakeIntensity + } + self.layoutIfNeeded() + }) { finished in + } + UIView.animate(withDuration: 0.2, delay: 0.1, options: [], animations: { + if toOn { + self.knobRightConstraint.constant = 1 + } else { + self.knobLeftConstraint.constant = 1 + } + self.layoutIfNeeded() + }) { finished in + self.valueShouldChange = true + } + } + + // MARK: - switch logic + public func setState(_ state: Bool, animated: Bool) { + setState(state, withoutBlockAnimated: animated) + if valueChangedBlock { + valueChangedBlock() + } + + 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() + } + } + + public func setState(_ state: Bool, withoutBlockAnimated animated: Bool) { + on = state + if !shouldTouchToSwitch { + knobEnlargeAnimation() + knobMoveAnitmationTo(on: on) + knobShakeAnitmationTo(on: on) + } + if on { + knobLeftConstraint.priority = 1 + knobRightConstraint.priority = 999 + knobRightConstraint.constant = 1 + } else { + knobRightConstraint.priority = 1 + knobLeftConstraint.priority = 999 + knobLeftConstraint.constant = 1 + } + + setBaseColorToOn(on, animated: animated) + knobReformAnimation(animated) + accessibilityValue = state ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff") + setNeedsLayout() + layoutIfNeeded() + } + + public func changeValue() -> Bool { + on ^= true + shouldTouchToSwitch = false + setState(on, animated: true) + shouldTouchToSwitch = true + sendActions(for: .valueChanged) + return on + } + } +} +