From 66255d9dc5329a4d2b482529ee20c00fe3a8b7ac Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 15:49:06 -0500 Subject: [PATCH] implemented initial vds toggle Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift | 343 +++--------------- .../Atomic/Atoms/Selectors/ToggleModel.swift | 60 +-- .../Atomic/Extensions/VDS-Enums+Codable.swift | 3 + 3 files changed, 91 insertions(+), 315 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 4341614e..dba501ca 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -8,6 +8,7 @@ import MVMCore import UIKit +import VDS public typealias ActionBlockConfirmation = () -> (Bool) @@ -19,53 +20,35 @@ 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 { - //-------------------------------------------------- +@objcMembers open class Toggle: VDS.Toggle, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol { + //------------------------------------------------------ // 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? + //------------------------------------------------------ + open var viewModel: ToggleModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + public var didToggleAction: ActionBlock? { + get { nil } + set { + if let action = newValue { + onChange = { _ in + action() + } + } else { + onChange = nil + } + } + } /// 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 { @@ -73,83 +56,16 @@ public typealias ActionBlockConfirmation = () -> (Bool) } /// The state on the toggle. Default value: false. - open var isOn: Bool = false { + open override var isOn: Bool { 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() - } - 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 + viewModel } - - //-------------------------------------------------- - // 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 //-------------------------------------------------- @@ -158,7 +74,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) super.init(frame: frame) } - public convenience override init() { + public convenience required init() { self.init(frame: .zero) } @@ -171,7 +87,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) /// - 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.isOn = isOn self.didToggleAction = didToggleAction } @@ -191,214 +107,71 @@ public typealias ActionBlockConfirmation = () -> (Bool) //-------------------------------------------------- // 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 + public func updateView(_ size: CGFloat) { } - heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height) - heightConstraint?.isActive = true + public override func setup() { + super.setup() + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return nil } + return MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled") + } - 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() + bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return nil } + return isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff") + } } 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() { - + toggle() + } + + open override func toggle() { if let result = shouldToggleAction?(), result { - isOn.toggle() + super.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() - } - } // MARK:- MoleculeViewProtocol - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - self.delegateObject = delegateObject + public func viewModelDidUpdate() { + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) - guard let model = model as? ToggleModel else { return } + isOn = viewModel.selected + isAnimated = viewModel.animated + isEnabled = viewModel.enabled && !viewModel.readOnly + showText = viewModel.showText + onText = viewModel.onText + offText = viewModel.offText + textSize = viewModel.textSize + textWeight = viewModel.textWeight + textPosition = viewModel.textPosition - 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 { + if let accessibileString = viewModel.accessibilityText { accessibilityLabel = accessibileString } - if model.action != nil || model.alternateAction != nil { + if viewModel.action != nil || viewModel.alternateAction != nil { didToggleAction = { [weak self] in guard let self = self else { return } if self.isOn { - if let action = model.action { + if let action = viewModel.action { MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) } } else { - if let action = model.alternateAction ?? model.action { + if let action = viewModel.alternateAction ?? viewModel.action { MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) } } @@ -406,8 +179,8 @@ public typealias ActionBlockConfirmation = () -> (Bool) } } - public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - Self.getContainerHeight() + public class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return Self().intrinsicContentSize.height } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index f4ce9234..a96bddb8 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -5,7 +5,7 @@ // Created by Scott Pfeil on 1/14/20. // Copyright © 2020 Verizon Wireless. All rights reserved. // - +import VDS public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- @@ -24,10 +24,13 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { 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 showText: Bool = false + public var onText: String = "On" + public var offText: String = "Off" + public var textSize: VDS.Toggle.TextSize = .small + public var textWeight: VDS.Toggle.TextWeight = .regular + public var textPosition: VDS.Toggle.TextPosition = .left public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName @@ -49,10 +52,14 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { case accessibilityIdentifier case alternateAction case accessibilityText - case onTintColor - case offTintColor - case onKnobTintColor - case offKnobTintColor + + case showText + case onText + case offText + case textSize + case textWeight + case textPosition + case fieldKey case groupName } @@ -102,25 +109,8 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { 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 @@ -130,6 +120,13 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + + showText = try typeContainer.decodeIfPresent(Bool.self, forKey: .showText) ?? false + onText = try typeContainer.decodeIfPresent(String.self, forKey: .onText) ?? "On" + offText = try typeContainer.decodeIfPresent(String.self, forKey: .offText) ?? "Off" + textSize = try typeContainer.decodeIfPresent(VDS.Toggle.TextSize.self, forKey: .textSize) ?? .small + textWeight = try typeContainer.decodeIfPresent(VDS.Toggle.TextWeight.self, forKey: .textWeight) ?? .regular + textPosition = try typeContainer.decodeIfPresent(VDS.Toggle.TextPosition.self, forKey: .textPosition) ?? .left } public func encode(to encoder: Encoder) throws { @@ -143,13 +140,16 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { 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) + + try container.encode(showText, forKey: .showText) + try container.encode(onText, forKey: .onText) + try container.encode(offText, forKey: .offText) + try container.encode(textSize, forKey: .textSize) + try container.encode(textWeight, forKey: .textWeight) + try container.encode(textPosition, forKey: .textPosition) } } diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index f4eb5500..1ae838e0 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -36,6 +36,9 @@ extension VDS.TextLinkCaret.IconPosition: Codable {} extension VDS.TileContainerBase.AspectRatio: Codable {} extension VDS.Tilelet.Padding: Codable {} extension VDS.TitleLockup.TextAlignment: Codable {} +extension VDS.Toggle.TextSize: Codable {} +extension VDS.Toggle.TextWeight: Codable {} +extension VDS.Toggle.TextPosition: Codable {} extension VDS.Tooltip.FillColor: Codable {} extension VDS.Tooltip.Size: Codable {} extension VDS.Line.Style: Codable {}