added a ViewModel intermediate class for testing

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2022-10-12 15:26:13 -05:00
parent b08923e7c0
commit 394f59b0c0
5 changed files with 783 additions and 0 deletions

View File

@ -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 = "<group>"; };
EA84F75028BD558F00D67ABC /* ConveienceAdherence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConveienceAdherence.swift; sourceTree = "<group>"; };
EAA5EEFD28F602FD003B3210 /* GenericMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMolecule.swift; sourceTree = "<group>"; };
EAA5EEFF28F74C43003B3210 /* VDSToggleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSToggleVM.swift; sourceTree = "<group>"; };
EAA5EF0128F74CE5003B3210 /* TestToggleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestToggleVM.swift; sourceTree = "<group>"; };
EAA658142875FA5E00484A7D /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = "<group>"; };
EACA5E5D2853DBC900CBA65B /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = "<group>"; };
EAF7F0902899825D00B287F5 /* TestLabelToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabelToggle.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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)

View File

@ -19,6 +19,12 @@
"stack": {
"moleculeName": "stack",
"molecules": [
{
"moleculeName": "stackItem",
"molecule": {
"moleculeName": "testToggle3"
}
},
{
"moleculeName": "stackItem",
"molecule": {

View File

@ -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<ToggleViewModelBase<TestToggleModel3>>, 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)
}
}

View File

@ -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<ModelType>
var viewModel: ViewModelType { get set }
var subscribers: Set<AnyCancellable> { 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<ModelType, Never> {
viewModel
.publisher
.debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
///-----------------------------------------------------------------------------
///MARK: -- ViewModel Protocol
///-----------------------------------------------------------------------------
public protocol ViewModel<ModelType>: AnyObject, Surfaceable, Disabling {
associatedtype ModelType: Modelable
var model: ModelType { get set }
var modelSubject: CurrentValueSubject<ModelType, Never> { get set }
var publisher: AnyPublisher<ModelType, Never> { get }
init(with model: ModelType)
func set(with model: ModelType)
}
///-----------------------------------------------------------------------------
///MARK: -- ViewModel Generic Base Class
///-----------------------------------------------------------------------------
public class ViewModelBase<ModelType: Modelable>: NSObject, ViewModel, ObservableObject {
public var model: ModelType
public var modelSubject = CurrentValueSubject<ModelType, Never>(ModelType())
public var publisher: AnyPublisher<ModelType, Never> { 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<ViewModelType: ViewModel>: UIControl, ViewModelHandler, ViewProtocol, Resettable {
public typealias ModelType = ViewModelType.ModelType
public var viewModel: ViewModelType = ViewModelType.init(with: ModelType())
//--------------------------------------------------
// MARK: - Combine Properties
//--------------------------------------------------
public var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// 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<ModelType: VDS.ToggleModel>: ViewModelBase<ModelType>, 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<ViewModelType: ToggleViewModel>: ControlViewModelHandler<ViewModelType> {
//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<ModelType, Never> {
viewModel
.publisher
.debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}