diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 4341614e..b229d12e 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,137 +20,40 @@ 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 - //-------------------------------------------------- + //------------------------------------------------------ + open var viewModel: ToggleModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? - /// 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? + public var didToggleAction: ActionBlock? { + didSet { + if let didToggleAction { + onChange = { _ in + didToggleAction() + } + } 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 { 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() - } - - 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 //-------------------------------------------------- @@ -158,7 +62,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) super.init(frame: frame) } - public convenience override init() { + public convenience required init() { self.init(frame: .zero) } @@ -171,7 +75,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,223 +95,75 @@ 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 - - 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() { + public func viewModelDidUpdate() { + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) - if let result = shouldToggleAction?(), result { - isOn.toggle() - didToggleAction?() + isOn = viewModel.selected + surface = viewModel.surface + isAnimated = viewModel.animated + isEnabled = viewModel.enabled && !viewModel.readOnly + showText = viewModel.showText + if let onText = viewModel.onText { + self.onText = onText } - } - - 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() + if let offText = viewModel.offText { + self.offText = offText } - } - - // MARK:- MoleculeViewProtocol - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - self.delegateObject = delegateObject + textSize = viewModel.textSize + textWeight = viewModel.textWeight + textPosition = viewModel.textPosition - 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 { + 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) } } } } } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + /// This will toggle the state of the Toggle and execute the actionBlock if provided. + public func toggleAndAction() { + toggle() + } - public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - Self.getContainerHeight() + open override func toggle() { + if let result = shouldToggleAction?(), result { + super.toggle() + viewModel?.selected = isOn + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + } + } + + //-------------------------------------------------- + // MARK:- MoleculeViewProtocol + //-------------------------------------------------- + + public func updateView(_ size: CGFloat) {} + + 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..c310e47c 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,15 @@ 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 surface: Surface { inverted ? .dark : .light } + public var inverted: Bool = false + public var showText: Bool = false + public var onText: String? + public var offText: String? + 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 +54,15 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { case accessibilityIdentifier case alternateAction case accessibilityText - case onTintColor - case offTintColor - case onKnobTintColor - case offKnobTintColor + + case inverted + case showText + case onText + case offText + case textSize + case textWeight + case textPosition + case fieldKey case groupName } @@ -102,25 +112,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 +123,14 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + + inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) ?? false + showText = try typeContainer.decodeIfPresent(Bool.self, forKey: .showText) ?? false + onText = try typeContainer.decodeIfPresent(String.self, forKey: .onText) + offText = try typeContainer.decodeIfPresent(String.self, forKey: .offText) + 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 +144,17 @@ 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(inverted, forKey: .inverted) + try container.encode(showText, forKey: .showText) + try container.encodeIfPresent(onText, forKey: .onText) + try container.encodeIfPresent(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/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 605f2e8a..82928c0f 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -34,11 +34,7 @@ public typealias ActionBlock = () -> () /// A specific text index to use as a unique marker. public var hero: Int? - - public var getRange: NSRange { - NSRange(location: 0, length: text?.count ?? 0) - } - + public var shouldMaskWhileRecording: Bool = false public var hasText: Bool { @@ -378,19 +374,24 @@ extension Label { // MARK: - Atomization extension Label { - + public func needsToBeConstrained() -> Bool { true } public func horizontalAlignment() -> UIStackView.Alignment { .leading } public func copyBackgroundColor() -> Bool { true } + } // MARK: - Multi-Link Functionality -extension Label { +extension VDS.Label { + + public var getRange: NSRange { + NSRange(location: 0, length: text?.count ?? 0) + } /// Underlines the tappable region and stores the tap logic for interation. - private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) { + internal func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) { guard let text, text.isValid(range: range) else { return } var textLink = ActionLabelAttribute(location: range.location, length: range.length) @@ -417,8 +418,16 @@ extension Label { return { [weak self] in guard let self = self else { return } - if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + if let button = self as? MFButtonProtocol { + if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(button, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, + additionalData: additionalData, + delegateObject: delegateObject) + } + } else { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, + additionalData: additionalData, + delegateObject: delegateObject) } } }