diff --git a/JSONCreator_iOS/JSONCreator.xcodeproj/project.pbxproj b/JSONCreator_iOS/JSONCreator.xcodeproj/project.pbxproj index 84e404d..3431806 100644 --- a/JSONCreator_iOS/JSONCreator.xcodeproj/project.pbxproj +++ b/JSONCreator_iOS/JSONCreator.xcodeproj/project.pbxproj @@ -95,6 +95,8 @@ EA84F75F28BD558F00D67ABC /* CustomWrappers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA84F74F28BD558F00D67ABC /* CustomWrappers.swift */; }; EA84F76028BD558F00D67ABC /* ConveienceAdherence.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA84F75028BD558F00D67ABC /* ConveienceAdherence.swift */; }; EAA5EEFE28F602FD003B3210 /* GenericMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEFD28F602FD003B3210 /* GenericMolecule.swift */; }; + EAA5EF0028F74C43003B3210 /* VDSToggleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEFF28F74C43003B3210 /* VDSToggleVM.swift */; }; + EAA5EF0228F74CE5003B3210 /* TestToggleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EF0128F74CE5003B3210 /* TestToggleVM.swift */; }; EAF7F0912899825D00B287F5 /* TestLabelToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0902899825D00B287F5 /* TestLabelToggle.swift */; }; EAF7F12528A15E2300B287F5 /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA3361FA2891D54A0071C351 /* VDSTypographyTokens.xcframework */; }; EAF7F12628A15E2300B287F5 /* VDSTypographyTokens.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA3361FA2891D54A0071C351 /* VDSTypographyTokens.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -207,6 +209,8 @@ EA84F74F28BD558F00D67ABC /* CustomWrappers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomWrappers.swift; sourceTree = ""; }; EA84F75028BD558F00D67ABC /* ConveienceAdherence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConveienceAdherence.swift; sourceTree = ""; }; EAA5EEFD28F602FD003B3210 /* GenericMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMolecule.swift; sourceTree = ""; }; + EAA5EEFF28F74C43003B3210 /* VDSToggleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSToggleVM.swift; sourceTree = ""; }; + EAA5EF0128F74CE5003B3210 /* TestToggleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestToggleVM.swift; sourceTree = ""; }; EAA658142875FA5E00484A7D /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = ""; }; EACA5E5D2853DBC900CBA65B /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = ""; }; EAF7F0902899825D00B287F5 /* TestLabelToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabelToggle.swift; sourceTree = ""; }; @@ -275,6 +279,8 @@ EA3361C0288B37FB0071C351 /* TestToggle.swift */, EA1B7BBC2893459E006AF0BC /* DecodableDefaults+VDS.swift */, EAF7F0902899825D00B287F5 /* TestLabelToggle.swift */, + EAA5EEFF28F74C43003B3210 /* VDSToggleVM.swift */, + EAA5EF0128F74CE5003B3210 /* TestToggleVM.swift */, EAA5EEFD28F602FD003B3210 /* GenericMolecule.swift */, ); path = JSONCreator; @@ -552,6 +558,8 @@ EA09CDFC282C430400A7835F /* CharacteristicModel.swift in Sources */, EA84F75328BD558F00D67ABC /* OptionalWrappers.swift in Sources */, D2B1E3F722F4A68F0065F95C /* DetailViewController.swift in Sources */, + EAA5EF0028F74C43003B3210 /* VDSToggleVM.swift in Sources */, + EAA5EF0228F74CE5003B3210 /* TestToggleVM.swift in Sources */, EA84F76028BD558F00D67ABC /* ConveienceAdherence.swift in Sources */, EA09CDDD282C40CC00A7835F /* GMFGSpeedTestHandler.swift in Sources */, EA09CDDF282C40CC00A7835F /* GMFGRouterWifiHandler.swift in Sources */, diff --git a/JSONCreator_iOS/JSONCreator/AppDelegate.swift b/JSONCreator_iOS/JSONCreator/AppDelegate.swift index 25cebc6..0fb42b7 100644 --- a/JSONCreator_iOS/JSONCreator/AppDelegate.swift +++ b/JSONCreator_iOS/JSONCreator/AppDelegate.swift @@ -133,6 +133,7 @@ extension AppDelegate { ModelRegistry.register(handler: TestLabelToggle.self, for: TestLabelToggleModel.self) ModelRegistry.register(handler: TestToggle.self, for: TestToggleModel.self) ModelRegistry.register(handler: TestToggle2.self, for: TestToggleModel2.self) + ModelRegistry.register(handler: TestToggle3.self, for: TestToggleModel3.self) ModelRegistry.register(handler: TextEntryField.self, for: TextEntryField64Model.self) ModelRegistry.register(handler: EmailVerifyField.self, for: EmailVerifyModel.self) ModelRegistry.register(handler: ToggleWifiActionHandler.self, for: ToggleWifiActionModel.self) diff --git a/JSONCreator_iOS/JSONCreator/JSON/Samples/FormContactInfo.json b/JSONCreator_iOS/JSONCreator/JSON/Samples/FormContactInfo.json index d0acf0a..0a71e14 100644 --- a/JSONCreator_iOS/JSONCreator/JSON/Samples/FormContactInfo.json +++ b/JSONCreator_iOS/JSONCreator/JSON/Samples/FormContactInfo.json @@ -19,6 +19,12 @@ "stack": { "moleculeName": "stack", "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "testToggle3" + } + }, { "moleculeName": "stackItem", "molecule": { diff --git a/JSONCreator_iOS/JSONCreator/TestToggleVM.swift b/JSONCreator_iOS/JSONCreator/TestToggleVM.swift new file mode 100644 index 0000000..e90240d --- /dev/null +++ b/JSONCreator_iOS/JSONCreator/TestToggleVM.swift @@ -0,0 +1,233 @@ +// +// TestToggleVM.swift +// JSONCreator +// +// Created by Matt Bruce on 10/12/22. +// Copyright © 2022 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore +import MVMCoreUI +import VDS + +public class TestToggleModel3: MoleculeModelProtocol, FormFieldProtocol, VDS.ToggleModel { + //ToggleModel + public var id = UUID() + public var showText: Bool = true + public var on: Bool = false + public var offText: String = "Off" + public var onText: String = "On" + public var textWeight: VDS.ToggleTextWeight = .bold + public var textSize: VDS.ToggleTextSize = .small + public var textPosition: VDS.ToggleTextPosition = .left + public var inputId: String? + public var value: AnyHashable? + public var dataAnalyticsTrack: String? + public var dataClickStream: String? + public var dataTrack: String? + public var accessibilityHintEnabled: String? + public var accessibilityHintDisabled: String? + public var accessibilityValueEnabled: String? + public var accessibilityValueDisabled: String? + public var accessibilityLabelEnabled: String? + public var accessibilityLabelDisabled: String? + public var surface: VDS.Surface = .light + public var disabled: Bool = false + public required init() {} + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String = "testToggle3" + public var backgroundColor: Color? //not used + + public var selected: Bool = false + public var enabled: Bool = true + public var readOnly: Bool = false + public var action: ActionModelProtocol? + public var alternateAction: ActionModelProtocol? + public var accessibilityText: String? + public var fieldKey: String? + public var groupName: String = FormValidator.defaultGroupName + public var baseValue: AnyHashable? + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case moleculeName + case state + case enabled + case readOnly + case action + case accessibilityIdentifier + case alternateAction + case accessibilityText + case fieldKey + case groupName + } + + //-------------------------------------------------- + // MARK: - Form Valdiation + //-------------------------------------------------- + + public func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } + return on + } + + //-------------------------------------------------- + // MARK: - Server Value + //-------------------------------------------------- + open func formFieldServerValue() -> AnyHashable? { + return formFieldValue() + } + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(_ state: Bool) { + selected = state + baseValue = state + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { + selected = state + on = state + } + action = try typeContainer.decodeModelIfPresent(codingKey: .action) + alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) + accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) + baseValue = selected + fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) + if let gName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + groupName = gName + } + enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true + disabled = !enabled + + 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(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(enabled, forKey: .enabled) + try container.encodeIfPresent(fieldKey, forKey: .fieldKey) + try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(readOnly, forKey: .readOnly) + } + + public static func == (lhs: TestToggleModel3, rhs: TestToggleModel3) -> Bool { + return lhs.id == rhs.id + && lhs.on == rhs.on + && lhs.showText == rhs.showText + && lhs.offText == rhs.offText + && lhs.onText == rhs.onText + && lhs.textWeight == rhs.textWeight + && lhs.textSize == rhs.textSize + && lhs.inputId == rhs.inputId + && lhs.value == rhs.value + && lhs.dataAnalyticsTrack == rhs.dataAnalyticsTrack + && lhs.dataClickStream == rhs.dataClickStream + && lhs.dataTrack == rhs.dataTrack + && lhs.accessibilityHintEnabled == rhs.accessibilityHintEnabled + && lhs.accessibilityHintDisabled == rhs.accessibilityHintDisabled + && lhs.accessibilityValueEnabled == rhs.accessibilityValueEnabled + && lhs.accessibilityValueDisabled == rhs.accessibilityValueDisabled + && lhs.accessibilityLabelEnabled == rhs.accessibilityLabelEnabled + && lhs.accessibilityLabelEnabled == rhs.accessibilityLabelEnabled + && lhs.surface == rhs.surface + && lhs.disabled == rhs.disabled + } +} + +open class TestToggle3: ToggleViewModelHandlerBase>, VDSVMMoleculeViewProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var delegateObject: MVMCoreUIDelegateObject? + public var additionalData: [AnyHashable: Any]? + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public override func initialSetup() { + super.initialSetup() + + publisher(for: .touchUpInside) + .sink {[weak self] toggle in + guard let self = self else { return } + 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) + } + + // MARK:- MVMCoreViewProtocol + open func updateView(_ size: CGFloat) {} + + open func viewModelDidSet() { + FormValidator.setupValidation(for: viewModel.model, delegate: delegateObject?.formHolderDelegate) + additionalData = additionalData.dictionaryAdding(key: KeySourceModel, value: viewModel) + } + + private func valueChanged(isOn: Bool){ + //tell the form you changed + _ = FormValidator.validate(delegate: self.delegateObject?.formHolderDelegate) + + if viewModel.model.action != nil || viewModel.model.alternateAction != nil { + var action: ActionModelProtocol? + if isOn { + action = viewModel.model.action + } else { + action = viewModel.model.alternateAction ?? viewModel.model.action + } + if let action { + MVMCoreUIActionHandler.performActionUnstructured(with: action, + sourceModel: viewModel.model, + additionalData: additionalData, + delegateObject: delegateObject) + } + } + + print("toggle value changed to: \(isOn)") + print("viewModel server value: \(viewModel.model.formFieldServerValue()!)") + } + + 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) + } +} diff --git a/JSONCreator_iOS/JSONCreator/VDSToggleVM.swift b/JSONCreator_iOS/JSONCreator/VDSToggleVM.swift new file mode 100644 index 0000000..12e4ee7 --- /dev/null +++ b/JSONCreator_iOS/JSONCreator/VDSToggleVM.swift @@ -0,0 +1,535 @@ +// +// VDSToggleVM.swift +// JSONCreator +// +// Created by Matt Bruce on 10/12/22. +// Copyright © 2022 Verizon Wireless. All rights reserved. +// +import Foundation +import UIKit +import VDSColorTokens +import Combine +import VDS +import MVMCore +import MVMCoreUI + +///----------------------------------------------------------------------------- +///MARK: -- VDSVMMoleculeViewProtocol (Contract between VDS -> Atomic +///----------------------------------------------------------------------------- +public protocol VDSVMMoleculeViewProtocol: MoleculeViewProtocol, MVMCoreViewProtocol, ViewModelHandler { + var delegateObject: MVMCoreUIDelegateObject? { get set } + var additionalData: [AnyHashable: Any]? { get set } + func viewModelDidSet() +} + +extension VDSVMMoleculeViewProtocol { + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + guard let castedModel = model as? ModelType else { return } + self.delegateObject = delegateObject + viewModel.set(with: castedModel) + viewModelDidSet() + } +} + +///----------------------------------------------------------------------------- +///MARK: -- ViewModelHandler Protocol +///----------------------------------------------------------------------------- +public protocol ViewModelHandler: AnyObject, Initable { + associatedtype ModelType: Modelable + associatedtype ViewModelType: ViewModel + var viewModel: ViewModelType { get set } + var subscribers: Set { get set } + init(with model: ModelType) + func set(with model: ModelType) + func shouldUpdateView(viewModel: ModelType) -> Bool + func updateView(viewModel: ModelType) +} + +extension ViewModelHandler { + + public init() { + self.init(with: ModelType()) + } + + public func set(with model: ModelType) { + if shouldUpdateView(viewModel: model){ + viewModel.set(with: model) + } + } + + public func shouldUpdateView(viewModel: ModelType) -> Bool { + self.viewModel.model != viewModel + } + + public func setupUpdateView() { + handlerPublisher() + .subscribe(on: RunLoop.main) + .sink { [weak self] viewModel in + self?.updateView(viewModel: viewModel) + } + .store(in: &subscribers) + } + + public func handlerPublisher() -> AnyPublisher { + viewModel + .publisher + .debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main) + .eraseToAnyPublisher() + } +} + +///----------------------------------------------------------------------------- +///MARK: -- ViewModel Protocol +///----------------------------------------------------------------------------- +public protocol ViewModel: AnyObject, Surfaceable, Disabling { + associatedtype ModelType: Modelable + var model: ModelType { get set } + var modelSubject: CurrentValueSubject { get set } + var publisher: AnyPublisher { get } + init(with model: ModelType) + func set(with model: ModelType) +} + +///----------------------------------------------------------------------------- +///MARK: -- ViewModel Generic Base Class +///----------------------------------------------------------------------------- +public class ViewModelBase: NSObject, ViewModel, ObservableObject { + public var model: ModelType + public var modelSubject = CurrentValueSubject(ModelType()) + public var publisher: AnyPublisher { modelSubject.eraseToAnyPublisher() } + + required public init(with model: ModelType) { + self.model = model + modelSubject.send(model) + } + + public func set(with model: ModelType){ + self.model = model + modelSubject.send(model) + } + + @Proxy(\.model.surface) + open var surface: Surface { didSet { modelSubject.send(model) }} + + @Proxy(\.model.disabled) + open var disabled: Bool { didSet { modelSubject.send(model) }} + +} + +///----------------------------------------------------------------------------- +///MARK: -- ControlViewModelHandler Generic Base Class (Old Control) +///----------------------------------------------------------------------------- +open class ControlViewModelHandler: UIControl, ViewModelHandler, ViewProtocol, Resettable { + public typealias ModelType = ViewModelType.ModelType + public var viewModel: ViewModelType = ViewModelType.init(with: ModelType()) + + //-------------------------------------------------- + // MARK: - Combine Properties + //-------------------------------------------------- + public var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + private var initialSetupPerformed = false + + @Proxy(\.viewModel.surface) + open var surface: Surface + + @Proxy(\.viewModel.disabled) + open var disabled: Bool { + didSet { + self.isEnabled = !disabled + } + } + + open override var isEnabled: Bool { + get { !viewModel.disabled } + set { + //create local vars for clear coding + let disabled = !newValue + if viewModel.disabled != disabled { + viewModel.disabled = disabled + } + isUserInteractionEnabled = isEnabled + } + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + initialSetup() + } + + public required init(with model: ModelType) { + super.init(frame: .zero) + initialSetup() + set(with: model) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + open func initialSetup() { + if !initialSetupPerformed { + initialSetupPerformed = true + setupUpdateView() + setup() + } + } + + override open func accessibilityActivate() -> Bool { + // Hold state in case User wanted isAnimated to remain off. + guard isUserInteractionEnabled else { return false } + sendActions(for: .touchUpInside) + return true + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open func updateView(viewModel: ModelType) { + fatalError("Implement updateView") + } + + open func reset() { + backgroundColor = .clear +// if let model = model as? Resettable { +// model.reset() +// } + } + + // MARK: - ViewProtocol + /// Will be called only once. + open func setup() { + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + } +} + +///----------------------------------------------------------------------------- +///MARK: -- Toggle +///----------------------------------------------------------------------------- + + +///----------------------------------------------------------------------------- +///MARK: -- ToggleViewModel Protocol +///----------------------------------------------------------------------------- +public protocol ToggleViewModel: ViewModel where ModelType: VDS.ToggleModel { + var isOn: Bool { get set } + var showText: Bool { get set } + var onText: String { get set } + var offText: String { get set } + var textSize: ToggleTextSize { get set } + var textWeight: ToggleTextWeight { get set } + var textPosition: ToggleTextPosition { get set } +} + +///----------------------------------------------------------------------------- +///MARK: -- ToggleViewModel Generic Base Class (for extending?) +///----------------------------------------------------------------------------- +public class ToggleViewModelBase: ViewModelBase, ToggleViewModel { + + @Proxy(\.model.on) + open var isOn: Bool { didSet { modelSubject.send(model) }} + + @Proxy(\.model.showText) + public var showText: Bool { didSet { modelSubject.send(model) }} + + @Proxy(\.model.onText) + public var onText: String { didSet { modelSubject.send(model) }} + + @Proxy(\.model.offText) + public var offText: String { didSet { modelSubject.send(model) }} + + @Proxy(\.model.textSize) + public var textSize: ToggleTextSize { didSet { modelSubject.send(model) }} + + @Proxy(\.model.textWeight) + public var textWeight: ToggleTextWeight { didSet { modelSubject.send(model) }} + + @Proxy(\.model.textPosition) + public var textPosition: ToggleTextPosition { didSet { modelSubject.send(model) }} +} + +///----------------------------------------------------------------------------- +///MARK: -- ToggleViewModelHandler Generic Base Class (for extending?) +///----------------------------------------------------------------------------- +open class ToggleViewModelHandlerBase: ControlViewModelHandler { + //Toggle + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var stackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + } + }() + + private var label = VDS.Label() + + private var toggleView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + private var knobView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .white + } + }() + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + public let toggleSize = CGSize(width: 52, height: 24) + public let toggleContainerSize = CGSize(width: 52, height: 44) + public let knobSize = CGSize(width: 20, height: 20) + + private var toggleColorConfiguration = BinaryDisabledSurfaceColorConfiguration().with { + $0.forTrue.enabled.lightColor = VDSColor.paletteGreen26 + $0.forTrue.enabled.darkColor = VDSColor.paletteGreen34 + $0.forTrue.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.forTrue.disabled.darkColor = VDSColor.interactiveDisabledOndark + $0.forFalse.enabled.lightColor = VDSColor.elementsSecondaryOnlight + $0.forFalse.enabled.darkColor = VDSColor.paletteGray44 + $0.forFalse.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.forFalse.disabled.darkColor = VDSColor.interactiveDisabledOndark + } + + private var knobColorConfiguration = BinaryDisabledSurfaceColorConfiguration().with { + $0.forTrue.enabled.lightColor = VDSColor.elementsPrimaryOndark + $0.forTrue.enabled.darkColor = VDSColor.elementsPrimaryOndark + $0.forTrue.disabled.lightColor = VDSColor.paletteGray95 + $0.forTrue.disabled.darkColor = VDSColor.paletteGray44 + $0.forFalse.enabled.lightColor = VDSColor.elementsPrimaryOndark + $0.forFalse.enabled.darkColor = VDSColor.elementsPrimaryOndark + $0.forFalse.disabled.lightColor = VDSColor.paletteGray95 + $0.forFalse.disabled.darkColor = VDSColor.paletteGray44 + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + @Proxy(\.viewModel.isOn) + open var isOn: Bool + + @Proxy(\.viewModel.showText) + public var showText: Bool + + @Proxy(\.viewModel.onText) + public var onText: String + + @Proxy(\.viewModel.offText) + public var offText: String + + @Proxy(\.viewModel.textSize) + public var textSize: ToggleTextSize + + @Proxy(\.viewModel.textWeight) + public var textWeight: ToggleTextWeight + + @Proxy(\.viewModel.textPosition) + public var textPosition: ToggleTextPosition + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var knobLeadingConstraint: NSLayoutConstraint? + private var knobTrailingConstraint: NSLayoutConstraint? + private var knobHeightConstraint: NSLayoutConstraint? + private var knobWidthConstraint: NSLayoutConstraint? + private var toggleHeightConstraint: NSLayoutConstraint? + private var toggleWidthConstraint: NSLayoutConstraint? + + //functions + //-------------------------------------------------- + // MARK: - Toggle + //-------------------------------------------------- + private func updateToggle(_ viewModel: ModelType) { + //private func + func constrainKnob(){ + self.knobLeadingConstraint?.isActive = false + self.knobTrailingConstraint?.isActive = false + if viewModel.on { + self.knobTrailingConstraint = self.toggleView.trailingAnchor.constraint(equalTo: self.knobView.trailingAnchor, constant: 2) + self.knobLeadingConstraint = self.knobView.leadingAnchor.constraint(greaterThanOrEqualTo: self.toggleView.leadingAnchor) + } else { + self.knobTrailingConstraint = self.toggleView.trailingAnchor.constraint(greaterThanOrEqualTo: self.knobView.trailingAnchor) + self.knobLeadingConstraint = self.knobView.leadingAnchor.constraint(equalTo: self.toggleView.leadingAnchor, constant: 2) + } + self.knobTrailingConstraint?.isActive = true + self.knobLeadingConstraint?.isActive = true + self.knobWidthConstraint?.constant = self.knobSize.width + self.layoutIfNeeded() + } + + let toggleColor = toggleColorConfiguration.getColor(viewModel) + let knobColor = knobColorConfiguration.getColor(viewModel) + + if viewModel.disabled { + toggleView.backgroundColor = toggleColor + knobView.backgroundColor = knobColor + constrainKnob() + } else { + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: { + self.toggleView.backgroundColor = toggleColor + self.knobView.backgroundColor = knobColor + }, completion: nil) + + UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: { + constrainKnob() + }, completion: nil) + } + } + + //-------------------------------------------------- + // MARK: - Labels + //-------------------------------------------------- + private func updateLabel(_ viewModel: ModelType) { + let showText = viewModel.showText + stackView.spacing = showText ? 12 : 0 + label.set(with: viewModel.labelModel) + + if stackView.subviews.contains(label) { + label.removeFromSuperview() + } + + if showText { + if textPosition == .left { + stackView.insertArrangedSubview(label, at: 0) + } else { + stackView.addArrangedSubview(label) + } + } + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func setup() { + super.setup() + + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + + //add tapGesture to self + publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in + self?.sendActions(for: .touchUpInside) + }.store(in: &subscribers) + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(stackView) + + //create the wrapping view + let toggleContainerView = UIView() + toggleContainerView.translatesAutoresizingMaskIntoConstraints = false + toggleContainerView.backgroundColor = .clear + toggleContainerView.widthAnchor.constraint(equalToConstant: toggleContainerSize.width).isActive = true + toggleContainerView.heightAnchor.constraint(equalToConstant: toggleContainerSize.height).isActive = true + + toggleHeightConstraint = toggleView.heightAnchor.constraint(equalToConstant: toggleSize.height) + toggleHeightConstraint?.isActive = true + + toggleWidthConstraint = toggleView.widthAnchor.constraint(equalToConstant: toggleSize.width) + toggleWidthConstraint?.isActive = true + + toggleView.layer.cornerRadius = toggleSize.height / 2.0 + knobView.layer.cornerRadius = knobSize.height / 2.0 + + toggleView.backgroundColor = toggleColorConfiguration.getColor(viewModel.model) + + toggleContainerView.addSubview(toggleView) + toggleView.addSubview(knobView) + + + knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: knobSize.height) + knobHeightConstraint?.isActive = true + knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: knobSize.width) + knobWidthConstraint?.isActive = true + knobView.centerYAnchor.constraint(equalTo: toggleView.centerYAnchor).isActive = true + knobView.topAnchor.constraint(greaterThanOrEqualTo: toggleView.topAnchor).isActive = true + + toggleView.bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true + + updateLabel(viewModel.model) + stackView.addArrangedSubview(toggleContainerView) + stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true + stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + toggleView.centerXAnchor.constraint(equalTo: toggleContainerView.centerXAnchor).isActive = true + toggleView.centerYAnchor.constraint(equalTo: toggleContainerView.centerYAnchor).isActive = true + + } + + public override func reset() { + super.reset() + toggleView.backgroundColor = toggleColorConfiguration.getColor(viewModel.model) + knobView.backgroundColor = knobColorConfiguration.getColor(viewModel.model) + } + + /// This will toggle the state of the Toggle and execute the actionBlock if provided. + open func toggle() { + isOn.toggle() + sendActions(for: .valueChanged) + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + open override func updateView(viewModel: ModelType) { + updateLabel(viewModel) + updateToggle(viewModel) + backgroundColor = viewModel.surface.color + setNeedsLayout() + layoutIfNeeded() + } + + public func set(with model: ModelType) { + if shouldUpdateView(viewModel: model){ + viewModel.set(with: model) + } + } + + public func shouldUpdateView(viewModel: ModelType) -> Bool { + self.viewModel.model != viewModel + } + + public func setupUpdateView() { + handlerPublisher() + .subscribe(on: RunLoop.main) + .sink { [weak self] viewModel in + self?.updateView(viewModel: viewModel) + } + .store(in: &subscribers) + } + + public func handlerPublisher() -> AnyPublisher { + viewModel + .publisher + .debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main) + .eraseToAnyPublisher() + } + +}