diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 85ad8e9c..99635900 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -8,8 +8,7 @@ import MVMCore import UIKit - -public typealias ActionBlockConfirmation = () -> (Bool) +import VDS /** A custom implementation of Apple's UISwitch. @@ -19,402 +18,102 @@ public typealias ActionBlockConfirmation = () -> (Bool) Container: The background of the toggle control. Knob: The circular indicator that slides on the container. */ -@objcMembers open class Toggle: Control, MVMCoreUIViewConstrainingProtocol { +open class Toggle: ToggleBase, VDSMoleculeViewProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - - /// Holds the on and off colors for the container. - public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack) - - /// Holds the on and off colors for the knob. - public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite) - - /// Holds the on and off colors for the disabled state.. - public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite) - - /// Set this flag to false if you do not want to animate state changes. - public var isAnimated = true - - public var didToggleAction: ActionBlock? - - /// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute. - public var shouldToggleAction: ActionBlockConfirmation? = { - return { true } - }() - - // Sizes are from InVision design specs. - static let containerSize = CGSize(width: 51, height: 31) - static let knobSize = CGSize(width: 28, height: 28) - - private var knobView: View = { - let view = View() - view.backgroundColor = .white - view.layer.cornerRadius = Toggle.getKnobHeight() / 2.0 - return view - }() - - //-------------------------------------------------- - // MARK: - Computed Properties - //-------------------------------------------------- - - open override var isEnabled: Bool { - didSet { - isUserInteractionEnabled = isEnabled - changeStateNoAnimation(isEnabled ? isOn : false) - setToggleAppearanceFromState() - accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled") - } - } - - /// Simple means to prevent user interaction with the toggle. - public var isLocked: Bool = false { - didSet { isUserInteractionEnabled = !isLocked } - } - - /// The state on the toggle. Default value: false. - open var isOn: Bool = false { - didSet { - if isAnimated { - UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: { - if self.isOn { - self.knobView.backgroundColor = self.knobTintColor.on - self.backgroundColor = self.containerTintColor.on - - } else { - self.knobView.backgroundColor = self.knobTintColor.off - self.backgroundColor = self.containerTintColor.off - } - }, completion: nil) - - UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: { - self.constrainKnob() - self.knobWidthConstraint?.constant = Self.getKnobWidth() - self.layoutIfNeeded() - }, completion: nil) - - } else { - setToggleAppearanceFromState() - self.constrainKnob() - } + public var viewModel: ToggleModel! + public var delegateObject: MVMCoreUIDelegateObject? + public var additionalData: [AnyHashable: Any]? - toggleModel?.selected = isOn - _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) - accessibilityValue = isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff") - setNeedsLayout() - layoutIfNeeded() - } - } - - public var toggleModel: ToggleModel? { - model as? ToggleModel - } - - //-------------------------------------------------- - // MARK: - Delegate - //-------------------------------------------------- - - private var delegateObject: MVMCoreUIDelegateObject? - - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - - private var knobLeadingConstraint: NSLayoutConstraint? - private var knobTrailingConstraint: NSLayoutConstraint? - private var knobHeightConstraint: NSLayoutConstraint? - private var knobWidthConstraint: NSLayoutConstraint? - private var heightConstraint: NSLayoutConstraint? - private var widthConstraint: NSLayoutConstraint? - - private func constrainKnob() { - - knobLeadingConstraint?.isActive = false - knobTrailingConstraint?.isActive = false - - _ = isOn ? constrainKnobOn() : constrainKnobOff() - - knobTrailingConstraint?.isActive = true - knobLeadingConstraint?.isActive = true - } - - private func constrainKnobOn() { - - knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2) - knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor) - } - - private func constrainKnobOff() { - - knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor) - knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2) - } - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- - - public override init(frame: CGRect) { - super.init(frame: frame) - } - - public convenience override init() { - self.init(frame: .zero) - } - public convenience init(isOn: Bool) { self.init(frame: .zero) self.isOn = isOn } - - /// - parameter isOn: Bool to set the state of the toggle. - /// - parameter didToggleAction: A closure which is executed after the toggle changes states. - public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) { - self.init(frame: .zero) - changeStateNoAnimation(isOn) - self.didToggleAction = didToggleAction - } - - /// - parameter shouldToggleAction: Takes a closure that returns a boolean. - /// - parameter didToggleAction: A closure which is executed after the toggle changes states. - public convenience init(shouldToggleAction: ActionBlockConfirmation?, didToggleAction: ActionBlock?) { - self.init(frame: .zero) - self.didToggleAction = didToggleAction - self.shouldToggleAction = shouldToggleAction - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - fatalError("Toggle does not support xib.") - } - - //-------------------------------------------------- - // MARK: - Lifecycle - //-------------------------------------------------- - - public override func updateView(_ size: CGFloat) { - super.updateView(size) - - heightConstraint?.constant = Self.getContainerHeight() - widthConstraint?.constant = Self.getContainerWidth() - - knobHeightConstraint?.constant = Self.getKnobHeight() - knobWidthConstraint?.constant = Self.getKnobWidth() - - layer.cornerRadius = Self.getContainerHeight() / 2.0 - knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0 - - changeStateNoAnimation(isOn) - } - - public override func setupView() { - super.setupView() - - isAccessibilityElement = true - accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint") - accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") - accessibilityTraits = .button - heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height) - heightConstraint?.isActive = true - - widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width) - widthConstraint?.isActive = true - - layer.cornerRadius = Self.getContainerHeight() / 2.0 - backgroundColor = containerTintColor.off - - addSubview(knobView) - - knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height) - knobHeightConstraint?.isActive = true - knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width) - knobWidthConstraint?.isActive = true - knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true - bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true - - constrainKnobOff() - } - - public override func reset() { - super.reset() - - backgroundColor = containerTintColor.off - knobView.backgroundColor = knobTintColor.off - accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") - isAnimated = true - didToggleAction = nil - shouldToggleAction = { return true } - } - - class func getContainerWidth() -> CGFloat { - let containerWidth = Self.containerSize.width - return (MFSizeObject(standardSize: containerWidth, standardiPadPortraitSize: CGFloat(Self.containerSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerWidth - } - - class func getContainerHeight() -> CGFloat { - let containerHeight = Self.containerSize.height - return (MFSizeObject(standardSize: containerHeight, standardiPadPortraitSize: CGFloat(Self.containerSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerHeight - } - - class func getKnobWidth() -> CGFloat { - let knobWidth = Self.knobSize.width - return (MFSizeObject(standardSize: knobWidth, standardiPadPortraitSize: CGFloat(Self.knobSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobWidth - } - - class func getKnobHeight() -> CGFloat { - let knobHeight = Self.knobSize.width - return (MFSizeObject(standardSize: knobHeight, standardiPadPortraitSize: CGFloat(Self.knobSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobHeight - } - - //-------------------------------------------------- - // 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 Toggle and execute the actionBlock if provided. - public func toggleAndAction() { - - if let result = shouldToggleAction?(), result { - isOn.toggle() - didToggleAction?() - } - } - - private func changeStateNoAnimation(_ state: Bool) { - - // Hold state in case User wanted isAnimated to remain off. - let isAnimatedState = isAnimated - - isAnimated = false - isOn = state - isAnimated = isAnimatedState - } - - override open func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } - let isAnimatedState = isAnimated - isAnimated = false - sendActions(for: .touchUpInside) - isAnimated = isAnimatedState - return true - } - - //-------------------------------------------------- - // MARK: - UIResponder - //-------------------------------------------------- - - open override func touchesBegan(_ touches: Set, with event: UIEvent?) { - - UIView.animate(withDuration: 0.1, animations: { - self.knobWidthConstraint?.constant += PaddingOne - self.layoutIfNeeded() - }) - } - - public override func touchesEnded(_ touches: Set, with event: UIEvent?) { - - knobReformAnimation() - - // Action only occurs of the user lifts up from withing acceptable region of the toggle. - guard let coordinates = touches.first?.location(in: self), - coordinates.x > -20, - coordinates.x < bounds.width + 20, - coordinates.y > -20, - coordinates.y < bounds.height + 20 - else { return } - - sendActions(for: .touchUpInside) - } - - public func touchesCancelled(_ touches: Set, with event: UIEvent) { - - knobReformAnimation() - sendActions(for: .touchCancel) - } - - //-------------------------------------------------- - // MARK: - Animations - //-------------------------------------------------- - - public func setToggleAppearanceFromState() { - - backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container - knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob - } - - public func knobReformAnimation() { - - if isAnimated { - UIView.animate(withDuration: 0.1, animations: { - self.knobWidthConstraint?.constant = Self.getKnobWidth() - self.layoutIfNeeded() - }, completion: nil) - - } else { - knobWidthConstraint?.constant = Self.getKnobWidth() - layoutIfNeeded() - } - } + open override func initialSetup() { + super.initialSetup() - // MARK:- MoleculeViewProtocol - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - self.delegateObject = delegateObject - - guard let model = model as? ToggleModel else { return } - - FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) - - containerTintColor.on = model.onTintColor.uiColor - containerTintColor.off = model.offTintColor.uiColor - knobTintColor.on = model.onKnobTintColor.uiColor - knobTintColor.off = model.offKnobTintColor.uiColor - isOn = model.selected - changeStateNoAnimation(isOn) - isAnimated = model.animated - isEnabled = model.enabled && !model.readOnly - - if let accessibileString = model.accessibilityText { - accessibilityLabel = accessibileString - } - - if model.action != nil || model.alternateAction != nil { - didToggleAction = { [weak self] in + publisher(for: .touchUpInside) + .sink {[weak self] toggle in guard let self = self else { return } - if self.isOn { - if let action = model.action { - MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) - } - } else { - if let action = model.alternateAction ?? model.action { - MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) - } - } + self.toggle() + }.store(in: &subscribers) + + publisher(for: .valueChanged) + .sink {[weak self] toggle in + guard let self = self else { return } + self.valueChanged(isOn: toggle.isOn) + }.store(in: &subscribers) + + accessibilityLabelEnabled = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") + accessibilityLabelDisabled = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") + accessibilityHintEnabled = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint") + accessibilityHintDisabled = MVMCoreUIUtility.hardcodedString(withKey: "AccDisabled") + accessibilityValueEnabled = MVMCoreUIUtility.hardcodedString(withKey: "AccOn") + accessibilityValueDisabled = MVMCoreUIUtility.hardcodedString(withKey: "AccOff") + } + + open func updateView(_ size: CGFloat) {} + + open override func updateView() { + super.updateView() + backgroundColor = .clear + } + + open func viewModelDidUpdate() { + guard let viewModel else { return } + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + additionalData = additionalData.dictionaryAdding(key: KeySourceModel, value: viewModel) + } + + private func valueChanged(isOn: Bool){ + guard let viewModel else { return } + //sync the value on the viewModel + viewModel.selected = isOn + + //tell the form you changed + _ = FormValidator.validate(delegate: self.delegateObject?.formHolderDelegate) + + if viewModel.action != nil || viewModel.alternateAction != nil { + var action: ActionModelProtocol? + if isOn { + action = viewModel.action + } else { + action = viewModel.alternateAction ?? viewModel.action + } + if let action { + MVMCoreUIActionHandler.performActionUnstructured(with: action, + sourceModel: viewModel, + additionalData: additionalData, + delegateObject: delegateObject) } } } - public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - Self.getContainerHeight() + public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return 44 + } + + private typealias ActionDefinition = (model: ActionModelProtocol, + sourceModel: MoleculeModelProtocol?) + + private func performActionUnstructured(definition: ActionDefinition) { + MVMCoreUIActionHandler.performActionUnstructured(with: definition.model, + sourceModel: definition.sourceModel, + additionalData: additionalData, + delegateObject: delegateObject) } } // MARK: - MVMCoreUIViewConstrainingProtocol -extension Toggle { +extension Toggle: MVMCoreUIViewConstrainingProtocol { public func needsToBeConstrained() -> Bool { true } public func horizontalAlignment() -> UIStackView.Alignment { .trailing } } + diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index e8d50851..3fd6c3d5 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -13,20 +13,14 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public static var identifier: String = "toggle" - public var accessibilityIdentifier: String? - public var backgroundColor: Color? + public var backgroundColor: Color? //not used + public var selected: Bool = false - public var animated: Bool = true public var enabled: Bool = true public var readOnly: Bool = false public var action: ActionModelProtocol? public var alternateAction: ActionModelProtocol? public var accessibilityText: String? - public var onTintColor: Color = Color(uiColor: .mvmGreen) - public var offTintColor: Color = Color(uiColor: .mvmBlack) - public var onKnobTintColor: Color = Color(uiColor: .mvmWhite) - public var offKnobTintColor: Color = Color(uiColor: .mvmWhite) - public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? @@ -34,22 +28,16 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- - + private enum CodingKeys: String, CodingKey { case moleculeName case state - case animated case enabled case readOnly case action - case backgroundColor case accessibilityIdentifier case alternateAction case accessibilityText - case onTintColor - case offTintColor - case onKnobTintColor - case offKnobTintColor case fieldKey case groupName } @@ -75,7 +63,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public init(_ state: Bool) { - self.selected = state + selected = state baseValue = state } @@ -87,60 +75,30 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { - self.selected = state + selected = state } - - if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) { - self.animated = animated - } - action = try typeContainer.decodeModelIfPresent(codingKey: .action) alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) - accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) - - if let onTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onTintColor) { - self.onTintColor = onTintColor - } - - if let offTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offTintColor) { - self.offTintColor = offTintColor - } - - if let onKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onKnobTintColor) { - self.onKnobTintColor = onKnobTintColor - } - - if let offKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offKnobTintColor) { - self.offKnobTintColor = offKnobTintColor - } - accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) - baseValue = selected fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { - self.groupName = groupName + if let gName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + groupName = gName } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + + } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(alternateAction, forKey: .alternateAction) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(selected, forKey: .state) - try container.encode(animated, forKey: .animated) try container.encode(enabled, forKey: .enabled) - try container.encode(onTintColor, forKey: .onTintColor) - try container.encode(onKnobTintColor, forKey: .onKnobTintColor) - try container.encode(onKnobTintColor, forKey: .onKnobTintColor) - try container.encode(offKnobTintColor, forKey: .offKnobTintColor) - try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) try container.encode(readOnly, forKey: .readOnly)