added a ViewModel intermediate class for testing
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
b08923e7c0
commit
394f59b0c0
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -19,6 +19,12 @@
|
||||
"stack": {
|
||||
"moleculeName": "stack",
|
||||
"molecules": [
|
||||
{
|
||||
"moleculeName": "stackItem",
|
||||
"molecule": {
|
||||
"moleculeName": "testToggle3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"moleculeName": "stackItem",
|
||||
"molecule": {
|
||||
|
||||
233
JSONCreator_iOS/JSONCreator/TestToggleVM.swift
Normal file
233
JSONCreator_iOS/JSONCreator/TestToggleVM.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
535
JSONCreator_iOS/JSONCreator/VDSToggleVM.swift
Normal file
535
JSONCreator_iOS/JSONCreator/VDSToggleVM.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user