Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui.git into feature/atomic-vds-checkbox
This commit is contained in:
commit
5a47b32789
@ -581,6 +581,8 @@
|
||||
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
|
||||
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
|
||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; };
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */; };
|
||||
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */; };
|
||||
EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
|
||||
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
|
||||
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
|
||||
@ -1205,6 +1207,8 @@
|
||||
EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
|
||||
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = "<group>"; };
|
||||
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEntryField.swift; sourceTree = "<group>"; };
|
||||
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
|
||||
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
|
||||
@ -1302,6 +1306,7 @@
|
||||
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
|
||||
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
|
||||
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */,
|
||||
);
|
||||
name = Rules;
|
||||
path = Rules/Rules;
|
||||
@ -2357,6 +2362,7 @@
|
||||
children = (
|
||||
0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */,
|
||||
0A21DB7E235DECC500C160A2 /* EntryField.swift */,
|
||||
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */,
|
||||
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
|
||||
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
|
||||
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
|
||||
@ -2845,6 +2851,7 @@
|
||||
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
|
||||
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
|
||||
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */,
|
||||
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
|
||||
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
|
||||
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
|
||||
@ -3148,6 +3155,7 @@
|
||||
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
|
||||
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
|
||||
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
|
||||
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */,
|
||||
D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */,
|
||||
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
|
||||
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,
|
||||
|
||||
@ -25,6 +25,7 @@ import VDS
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool?
|
||||
public var errorMessage: String?
|
||||
public var initialErrorMessage: String?
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
@ -89,9 +90,11 @@ import VDS
|
||||
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
} else {
|
||||
self.errorMessage = initialErrorMessage
|
||||
}
|
||||
|
||||
self.isValid = valid
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
@ -103,6 +106,7 @@ import VDS
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
initialErrorMessage = errorMessage
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
|
||||
@ -345,9 +345,8 @@ import UIKit
|
||||
|
||||
numberOfDigits = model.digits
|
||||
|
||||
if let entryType = model.type {
|
||||
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
||||
}
|
||||
let entryType = model.type
|
||||
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
||||
|
||||
let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self
|
||||
|
||||
|
||||
@ -7,19 +7,66 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: ItemDropdownEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
open var pickerData: [String] = [] {
|
||||
didSet {
|
||||
options = pickerData.compactMap({ DropdownOptionModel(text: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
private var isEditting: Bool = false
|
||||
|
||||
open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
/// Closure passed here will run as picker changes items.
|
||||
public var observeDropdownChange: ((String?, String) -> ())?
|
||||
|
||||
open var pickerData: [String] = []
|
||||
/// Closure passed here will run upon dismissing the selection picker.
|
||||
public var observeDropdownSelection: ((String) -> ())?
|
||||
|
||||
public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? {
|
||||
model as? ItemDropdownEntryFieldModel
|
||||
/// When selecting for first responder, allow initial selected value to appear in empty text field.
|
||||
public var setInitialValueInTextField = true
|
||||
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
set {}
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
open weak var uiTextFieldDelegate: UITextFieldDelegate? {
|
||||
get { dropdownField.delegate }
|
||||
set { dropdownField.delegate = newValue }
|
||||
}
|
||||
|
||||
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||
_ = resignFirstResponder()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -28,7 +75,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
@objc public convenience init() {
|
||||
@objc public convenience required init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@ -40,76 +87,134 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
@objc required public init?(coder: NSCoder) {
|
||||
fatalError("ItemDropdownEntryField init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.init(model: model, delegateObject, additionalData)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let selectedItem else { return }
|
||||
viewModel.selectedIndex = control.selectId
|
||||
observeDropdownSelection?(selectedItem.text)
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
setInitialValueFromPicker()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
if let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
performDropdownAction()
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
pickerData = viewModel.options
|
||||
showInlineLabel = viewModel.showInlineLabel
|
||||
helperTextPlacement = viewModel.feedbackTextPlacement
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
if let index = viewModel.selectedIndex {
|
||||
selectId = index
|
||||
optionsPicker.selectRow(index, inComponent: 0, animated: false)
|
||||
pickerView(optionsPicker, didSelectRow: index, inComponent: 0)
|
||||
}
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
selectId = nil
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func updateView(_ size: CGFloat) { }
|
||||
|
||||
/// Sets the textField with the first value of the available picker data.
|
||||
@objc private func setInitialValueFromPicker() {
|
||||
private func setInitialValueFromPicker() {
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
if setInitialValueInTextField {
|
||||
let pickerIndex = pickerView.selectedRow(inComponent: 0)
|
||||
itemDropdownEntryFieldModel?.selectedIndex = pickerIndex
|
||||
observeDropdownChange?(text, pickerData[pickerIndex])
|
||||
text = pickerData[pickerIndex]
|
||||
let pickerIndex = optionsPicker.selectedRow(inComponent: 0)
|
||||
viewModel.selectedIndex = pickerIndex
|
||||
selectId = pickerIndex
|
||||
observeDropdownChange?(selectedItem?.text, pickerData[pickerIndex])
|
||||
}
|
||||
}
|
||||
|
||||
@objc override func startEditing() {
|
||||
super.startEditing()
|
||||
|
||||
setInitialValueFromPicker()
|
||||
private func performDropdownAction() {
|
||||
guard let actionModel = viewModel.action,
|
||||
!dropdownField.isFirstResponder
|
||||
else { return }
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
@objc override func endInputing() {
|
||||
super.endInputing()
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)])
|
||||
}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
guard let model = model as? ItemDropdownEntryFieldModel else { return }
|
||||
|
||||
pickerData = model.options
|
||||
|
||||
if let index = model.selectedIndex {
|
||||
self.pickerView.selectRow(index, inComponent: 0, animated: false)
|
||||
self.pickerView(pickerView, didSelectRow: index, inComponent: 0)
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Picker Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
||||
|
||||
@objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
pickerData.count
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
guard !pickerData.isEmpty else { return nil }
|
||||
|
||||
return pickerData[row]
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
itemDropdownEntryFieldModel?.selectedIndex = row
|
||||
observeDropdownChange?(text, pickerData[row])
|
||||
text = pickerData[row]
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,19 @@
|
||||
// Created by Kevin Christiano on 1/22/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel {
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public override class var identifier: String { "dropDown" }
|
||||
|
||||
public var action: ActionModelProtocol?
|
||||
public var options: [String] = []
|
||||
public var selectedIndex: Int?
|
||||
public var showInlineLabel: Bool = false
|
||||
public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom
|
||||
|
||||
public init(with options: [String], selectedIndex: Int? = nil) {
|
||||
self.options = options
|
||||
@ -42,6 +45,9 @@
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case options
|
||||
case selectedIndex
|
||||
case action
|
||||
case showInlineLabel
|
||||
case feedbackTextPlacement
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -58,6 +64,9 @@
|
||||
self.selectedIndex = selectedIndex
|
||||
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil
|
||||
}
|
||||
showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false
|
||||
feedbackTextPlacement = try typeContainer.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
@ -65,5 +74,8 @@
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(options, forKey: .options)
|
||||
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
|
||||
try container.encode(showInlineLabel, forKey: .showInlineLabel)
|
||||
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,7 +315,9 @@ import UIKit
|
||||
self.showError = false
|
||||
}
|
||||
self.isEnabled = model.enabled
|
||||
self.text = model.text
|
||||
if let text = model.text, !text.isEmpty {
|
||||
self.text = model.text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,347 @@
|
||||
//
|
||||
// InputEntryField.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/16/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class InputEntryField: VDS.InputField, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: TextEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
/// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well.
|
||||
private weak var proprietorTextDelegate: UITextFieldDelegate?
|
||||
|
||||
private var isEditting: Bool = false {
|
||||
didSet {
|
||||
viewModel.selected = isEditting
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
private var observingForChange: Bool = false
|
||||
|
||||
/// Validate when user resigns editing. Default: true
|
||||
open var validateWhenDoneEditing: Bool = true
|
||||
|
||||
open var shouldMaskWhileRecording: Bool {
|
||||
return viewModel.shouldMaskRecordedView ?? false
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The text of this TextField.
|
||||
open override var text: String? {
|
||||
didSet {
|
||||
viewModel?.text = text
|
||||
}
|
||||
}
|
||||
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
/// Placeholder access for the TextField.
|
||||
public var placeholder: String? {
|
||||
get { textField.placeholder }
|
||||
set { textField.placeholder = newValue }
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate Properties
|
||||
//--------------------------------------------------
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
open weak var uiTextFieldDelegate: UITextFieldDelegate?
|
||||
{
|
||||
get { textField.delegate }
|
||||
set {
|
||||
textField.delegate = self
|
||||
proprietorTextDelegate = newValue
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
//turn off internal required rule
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self else { return }
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
if (viewModel.type == .email) {
|
||||
// remove spaces (either user entered Or auto-correct suggestion) for the email field
|
||||
text = textField.text?.replacingOccurrences(of: " ", with: "")
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
if viewModel.clearTextOnTap {
|
||||
text = ""
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
if validateWhenDoneEditing, let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
regexTextFieldOutputIfAvailable()
|
||||
|
||||
}.store(in: &subscribers)
|
||||
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
if let viewModel {
|
||||
switch viewModel.type {
|
||||
case .secure:
|
||||
textField.isSecureTextEntry = true
|
||||
textField.shouldMaskWhileRecording = true
|
||||
|
||||
case .numberSecure:
|
||||
textField.isSecureTextEntry = true
|
||||
textField.shouldMaskWhileRecording = true
|
||||
textField.keyboardType = .numberPad
|
||||
|
||||
case .email:
|
||||
textField.keyboardType = .emailAddress
|
||||
|
||||
case .securityCode, .creditCard, .password:
|
||||
textField.shouldMaskWhileRecording = true
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Override the preset keyboard set in type.
|
||||
if let keyboardType = viewModel.assignKeyboardType() {
|
||||
textField.keyboardType = keyboardType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func viewModelDidUpdate() {
|
||||
|
||||
fieldType = viewModel.type.toVDSFieldType()
|
||||
text = viewModel.text
|
||||
placeholder = viewModel.placeholder
|
||||
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
containerView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||
textField.textAlignment = viewModel.textAlignment
|
||||
textField.enableClipboardActions = viewModel.enableClipboardActions
|
||||
textField.placeholder = viewModel.placeholder ?? ""
|
||||
uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate
|
||||
observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
viewModel.rules = rules
|
||||
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
text = ""
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
//Added to override text when view is reloaded.
|
||||
if let text = viewModel.text, !text.isEmpty {
|
||||
regexTextFieldOutputIfAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Observing for Change (TextFieldDelegate)
|
||||
//--------------------------------------------------
|
||||
@objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) {
|
||||
observingTextFieldDelegate = delegate
|
||||
uiTextFieldDelegate = delegate
|
||||
}
|
||||
|
||||
func regexTextFieldOutputIfAvailable() {
|
||||
|
||||
if let regex = viewModel?.displayFormat,
|
||||
let mask = viewModel?.displayMask,
|
||||
let finalText = text {
|
||||
|
||||
let range = NSRange(finalText.startIndex..., in: finalText)
|
||||
|
||||
if let regex = try? NSRegularExpression(pattern: regex) {
|
||||
let maskedText = regex.stringByReplacingMatches(in: finalText,
|
||||
range: range,
|
||||
withTemplate: mask)
|
||||
textField.text = maskedText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||
_ = resignFirstResponder()
|
||||
}
|
||||
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
@objc open func updateView(_ size: CGFloat) {}
|
||||
}
|
||||
|
||||
extension InputEntryField {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Implemented TextField Delegate
|
||||
//--------------------------------------------------
|
||||
@discardableResult
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
??
|
||||
super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
}
|
||||
|
||||
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidBeginEditing?(textField) ?? super.textFieldDidBeginEditing(textField)
|
||||
}
|
||||
|
||||
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidEndEditing?(textField) ?? super.textFieldDidEndEditing(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
extension InputEntryField {
|
||||
|
||||
@objc open func pushAccessibilityNotification() {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ViewMasking {
|
||||
static var shouldMaskWhileRecording: UInt8 = 0
|
||||
}
|
||||
|
||||
extension VDS.TextField: ViewMaskingProtocol {
|
||||
public var shouldMaskWhileRecording: Bool {
|
||||
get {
|
||||
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import MVMCore
|
||||
/**
|
||||
This class provides the convenience of formatting the MDN entered/displayer for the user.
|
||||
*/
|
||||
@objcMembers open class MdnEntryField: TextEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
||||
@objcMembers open class MdnEntryField: InputEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
@ -47,52 +47,17 @@ import MVMCore
|
||||
get { MVMCoreUIUtility.removeMdnFormat(text) }
|
||||
set { text = MVMCoreUIUtility.formatMdn(newValue) }
|
||||
}
|
||||
|
||||
/// Toggles selected or original (unselected) UI.
|
||||
public override var isSelected: Bool {
|
||||
get { return entryFieldContainer.isSelected }
|
||||
set (selected) {
|
||||
if selected && showError {
|
||||
showError = false
|
||||
}
|
||||
|
||||
super.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
@objc public convenience init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@objc required public init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
fatalError("MdnEntryField xib not supported.")
|
||||
}
|
||||
|
||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.init(model: model, delegateObject, additionalData)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Setup
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override func setupFieldContainerContent(_ container: UIView) {
|
||||
super.setupFieldContainerContent(container)
|
||||
|
||||
textField.keyboardType = .numberPad
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
setupTextFieldToolbar()
|
||||
}
|
||||
|
||||
open override func setupTextFieldToolbar() {
|
||||
|
||||
open func setupTextFieldToolbar() {
|
||||
let toolbar = UIToolbar.createEmptyToolbar()
|
||||
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let contacts = UIBarButtonItem(title: MVMCoreUIUtility.hardcodedString(withKey: "textfield_contacts_barbutton"), style: .plain, target: self, action: #selector(getContacts))
|
||||
@ -103,40 +68,7 @@ import MVMCore
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public func hasValidMDN() -> Bool {
|
||||
|
||||
guard let MDN = mdn, !MDN.isEmpty else { return false }
|
||||
|
||||
if isNationalMDN {
|
||||
return MVMCoreUIUtility.validateMDNString(MDN)
|
||||
}
|
||||
|
||||
return MVMCoreUIUtility.validateInternationalMDNString(MDN)
|
||||
}
|
||||
|
||||
@objc public func validateMDNTextField() -> Bool {
|
||||
|
||||
guard !shouldValidateMDN, let MDN = mdn, !MDN.isEmpty else {
|
||||
isValid = true
|
||||
return true
|
||||
}
|
||||
|
||||
isValid = hasValidMDN()
|
||||
|
||||
if self.isValid {
|
||||
showError = false
|
||||
|
||||
} else {
|
||||
entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message")
|
||||
showError = true
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: textField)
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@objc public func getContacts(_ sender: Any?) {
|
||||
|
||||
let picker = CNContactPickerViewController()
|
||||
@ -152,11 +84,12 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
textField.keyboardType = .phonePad
|
||||
public override func viewModelDidUpdate() {
|
||||
viewModel.type = .phone
|
||||
super.viewModelDidUpdate()
|
||||
if let phoneNumber = viewModel.text {
|
||||
text = phoneNumber.formatUSNumber()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -179,62 +112,47 @@ import MVMCore
|
||||
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
|
||||
unformattedMDN = String(unformedMDN[startIndex...])
|
||||
}
|
||||
|
||||
text = unformattedMDN
|
||||
textFieldShouldReturn(textField)
|
||||
textFieldDidEndEditing(textField)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Implemented TextField Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
@discardableResult
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
|
||||
textField.resignFirstResponder()
|
||||
|
||||
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
||||
@objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
_ = resignFirstResponder()
|
||||
let superValue = super.textFieldShouldReturn(textField)
|
||||
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? superValue
|
||||
}
|
||||
|
||||
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
|
||||
if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) {
|
||||
return false
|
||||
}
|
||||
|
||||
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
|
||||
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
let superValue = super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? superValue
|
||||
}
|
||||
|
||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
|
||||
textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text)
|
||||
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
super.textFieldDidBeginEditing(textField)
|
||||
proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
|
||||
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidEndEditing?(textField)
|
||||
|
||||
if validateMDNTextField() {
|
||||
if isNationalMDN {
|
||||
textField.text = MVMCoreUIUtility.formatMdn(textField.text)
|
||||
}
|
||||
// Validate the base input field along with triggering form field validation rules.
|
||||
validateText()
|
||||
}
|
||||
super.textFieldDidEndEditing(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,4 +12,9 @@
|
||||
//--------------------------------------------------
|
||||
|
||||
public override class var identifier: String { "mdnEntryField" }
|
||||
|
||||
open override func formFieldServerValue() -> AnyHashable? {
|
||||
guard let value = formFieldValue() as? String else { return nil }
|
||||
return value.filter { $0.isNumber }
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ import UIKit
|
||||
|
||||
@objc public protocol ObservingTextFieldDelegate {
|
||||
/// Called when the entered text becomes valid based on the validation block
|
||||
@objc optional func isValid(textfield: TextEntryField?)
|
||||
@objc optional func isValid(textfield: Any?)
|
||||
/// Called when the entered text becomes invalid based on the validation block
|
||||
@objc optional func isInvalid(textfield: TextEntryField?)
|
||||
@objc optional func isInvalid(textfield: Any?)
|
||||
/// Dismisses the keyboard.
|
||||
@objc optional func dismissFieldInput(_ sender: Any?)
|
||||
}
|
||||
@ -317,9 +317,9 @@ import UIKit
|
||||
super.shouldShowError(showError)
|
||||
|
||||
if showError {
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
} else {
|
||||
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||
} else {
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
// Created by Kevin Christiano on 1/22/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
@objcMembers open class TextEntryFieldModel: EntryFieldModel {
|
||||
@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatableProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Types
|
||||
//--------------------------------------------------
|
||||
@ -20,6 +21,39 @@
|
||||
case email
|
||||
case text
|
||||
case phone
|
||||
|
||||
//additional
|
||||
case inlineAction
|
||||
case creditCard
|
||||
case date
|
||||
case securityCode
|
||||
|
||||
public func toVDSFieldType() -> VDS.InputField.FieldType {
|
||||
switch self {
|
||||
case .password:
|
||||
.password
|
||||
case .secure:
|
||||
.text
|
||||
case .number:
|
||||
.number
|
||||
case .numberSecure:
|
||||
.number
|
||||
case .email:
|
||||
.text
|
||||
case .text:
|
||||
.text
|
||||
case .phone:
|
||||
.telephone
|
||||
case .inlineAction:
|
||||
.inlineAction
|
||||
case .creditCard:
|
||||
.creditCard
|
||||
case .date:
|
||||
.date
|
||||
case .securityCode:
|
||||
.securityCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -33,12 +67,21 @@
|
||||
public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var textAlignment: NSTextAlignment = .left
|
||||
public var keyboardOverride: String?
|
||||
public var type: EntryType?
|
||||
public var type: EntryType = .text
|
||||
public var clearTextOnTap: Bool = false
|
||||
public var displayFormat: String?
|
||||
public var displayMask: String?
|
||||
public var enableClipboardActions: Bool = true
|
||||
|
||||
public var tooltip: TooltipModel?
|
||||
public var transparentBackground: Bool = false
|
||||
public var width: CGFloat?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - FormFieldInternalValidatableProtocol
|
||||
//--------------------------------------------------
|
||||
open var rules: [AnyRule<String>]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -114,6 +157,9 @@
|
||||
case displayFormat
|
||||
case displayMask
|
||||
case enableClipboardActions
|
||||
case tooltip
|
||||
case transparentBackground
|
||||
case width
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -128,7 +174,7 @@
|
||||
displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat)
|
||||
keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride)
|
||||
displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask)
|
||||
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type)
|
||||
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) ?? .text
|
||||
|
||||
if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) {
|
||||
self.clearTextOnTap = clearTextOnTap
|
||||
@ -149,6 +195,10 @@
|
||||
if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) {
|
||||
self.enableClipboardActions = enableClipboardActions
|
||||
}
|
||||
|
||||
tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip)
|
||||
transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false
|
||||
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||
}
|
||||
|
||||
open override func encode(to encoder: Encoder) throws {
|
||||
@ -164,5 +214,8 @@
|
||||
try container.encode(disabledTextColor, forKey: .disabledTextColor)
|
||||
try container.encode(clearTextOnTap, forKey: .clearTextOnTap)
|
||||
try container.encode(enableClipboardActions, forKey: .enableClipboardActions)
|
||||
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||
try container.encode(transparentBackground, forKey: .transparentBackground)
|
||||
try container.encodeIfPresent(width, forKey: .width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,100 +7,60 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
|
||||
class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Outlets
|
||||
//--------------------------------------------------
|
||||
|
||||
open private(set) var textView: TextView = {
|
||||
let textView = TextView()
|
||||
textView.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return textView
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
//------------------------------------------------------
|
||||
open var viewModel: TextViewEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
private var observingForChange: Bool = false
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
private var isEditting: Bool = false {
|
||||
didSet {
|
||||
viewModel.selected = isEditting
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var textViewEntryFieldModel: TextViewEntryFieldModel? {
|
||||
model as? TextViewEntryFieldModel
|
||||
open var shouldMaskWhileRecording: Bool {
|
||||
return viewModel.shouldMaskRecordedView ?? false
|
||||
}
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
get { super.isEnabled }
|
||||
set (enabled) {
|
||||
super.isEnabled = enabled
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.textView.isEnabled = enabled
|
||||
if self.textView.isShowingPlaceholder {
|
||||
self.textView.textColor = self.textView.placeholderTextColor
|
||||
} else {
|
||||
self.textView.textColor = (self.textView.isEnabled ? self.textViewEntryFieldModel?.enabledTextColor : self.textViewEntryFieldModel?.disabledTextColor)?.uiColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var showError: Bool {
|
||||
get { super.showError }
|
||||
set (error) {
|
||||
|
||||
if error {
|
||||
textView.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "")
|
||||
} else {
|
||||
textView.accessibilityValue = nil
|
||||
}
|
||||
|
||||
super.showError = error
|
||||
|
||||
/// Placeholder access for the textView.
|
||||
open var placeholder: String? {
|
||||
get { viewModel?.placeholder }
|
||||
set {
|
||||
textView.placeholder = newValue ?? ""
|
||||
viewModel?.placeholder = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of this textView.
|
||||
open override var text: String? {
|
||||
get { textViewEntryFieldModel?.text }
|
||||
set {
|
||||
textView.text = newValue
|
||||
textViewEntryFieldModel?.text = newValue
|
||||
didSet {
|
||||
viewModel?.text = text
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder access for the textView.
|
||||
public var placeholder: String? {
|
||||
get { textViewEntryFieldModel?.placeholder }
|
||||
set {
|
||||
textView.placeholder = newValue ?? ""
|
||||
textViewEntryFieldModel?.placeholder = newValue
|
||||
textView.setPlaceholderIfAvailable()
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraint
|
||||
//--------------------------------------------------
|
||||
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
private var topConstraint: NSLayoutConstraint?
|
||||
private var leadingConstraint: NSLayoutConstraint?
|
||||
private var trailingConstraint: NSLayoutConstraint?
|
||||
private var bottomConstraint: NSLayoutConstraint?
|
||||
|
||||
private func adjustMarginConstraints(constant: CGFloat) {
|
||||
|
||||
topConstraint?.constant = constant
|
||||
leadingConstraint?.constant = constant
|
||||
trailingConstraint?.constant = constant
|
||||
bottomConstraint?.constant = constant
|
||||
set {}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -108,198 +68,178 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextViewDelegate: ObservingTextFieldDelegate? {
|
||||
didSet {
|
||||
if observingTextViewDelegate != nil && !observingForChange {
|
||||
observingForChange = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextView.textDidChangeNotification, object: textView)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextView.textDidEndEditingNotification, object: textView)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextView.textDidBeginEditingNotification, object: textView)
|
||||
|
||||
} else if observingTextViewDelegate == nil && observingForChange {
|
||||
observingForChange = false
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: textView)
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: textView)
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: textView)
|
||||
}
|
||||
}
|
||||
}
|
||||
open weak var observingTextViewDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
public weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||
open weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||
get { textView.delegate }
|
||||
set { textView.delegate = newValue }
|
||||
}
|
||||
|
||||
@objc public func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
||||
@objc open func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
||||
observingTextViewDelegate = delegate
|
||||
uiTextViewDelegate = delegate
|
||||
}
|
||||
|
||||
open func setupTextViewToolbar() {
|
||||
let observingDelegate = observingTextViewDelegate ?? self
|
||||
textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
|
||||
action: #selector(observingDelegate.dismissFieldInput))
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc open override func setupFieldContainerContent(_ container: UIView) {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
//turn off internal required rule
|
||||
useRequiredRule = false
|
||||
|
||||
container.addSubview(textView)
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self else { return }
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three)
|
||||
leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three)
|
||||
trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three)
|
||||
bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three)
|
||||
textView
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
|
||||
}.store(in: &subscribers)
|
||||
|
||||
topConstraint?.isActive = true
|
||||
leadingConstraint?.isActive = true
|
||||
trailingConstraint?.isActive = true
|
||||
bottomConstraint?.isActive = true
|
||||
|
||||
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0)
|
||||
accessibilityElements = [textView]
|
||||
textView
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
if let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
textView.updateView(size)
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
textView.reset()
|
||||
adjustMarginConstraints(constant: Padding.Three)
|
||||
heightConstraint?.constant = 0
|
||||
heightConstraint?.isActive = false
|
||||
}
|
||||
open func viewModelDidUpdate() {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Validates the text of the entry field.
|
||||
@objc public override func validateText() {
|
||||
text = textView.text
|
||||
super.validateText()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidBeginEditingNotification
|
||||
@objc override func startEditing() {
|
||||
super.startEditing()
|
||||
_ = textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidChangeNotification (each character entry)
|
||||
@objc override func valueChanged() {
|
||||
super.valueChanged()
|
||||
validateText()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidEndEditingNotification
|
||||
@objc override func endInputing() {
|
||||
super.endInputing()
|
||||
text = viewModel.text
|
||||
minHeight = viewModel.minHeight
|
||||
maxLength = viewModel.maxLength
|
||||
|
||||
// Don't show error till user starts typing.
|
||||
guard text?.count ?? 0 != 0 else {
|
||||
showError = false
|
||||
return
|
||||
}
|
||||
|
||||
if let isValid = textViewEntryFieldModel?.isValid {
|
||||
self.isValid = isValid
|
||||
}
|
||||
|
||||
showError = !isValid
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
guard let model = model as? TextViewEntryFieldModel else { return }
|
||||
|
||||
if let height = model.height {
|
||||
heightConstraint?.constant = height
|
||||
heightConstraint?.isActive = true
|
||||
}
|
||||
|
||||
text = model.text
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
||||
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
||||
|
||||
if let accessibilityText = model.accessibilityText {
|
||||
if let accessibilityText = viewModel.accessibilityText {
|
||||
accessibilityLabel = accessibilityText
|
||||
}
|
||||
containerView.accessibilityIdentifier = viewModel.accessibilityIdentifier
|
||||
textView.isEditable = viewModel.editable
|
||||
textView.textAlignment = viewModel.textAlignment
|
||||
textView.placeholder = viewModel.placeholder ?? ""
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
textView.isEditable = model.editable
|
||||
textView.textAlignment = model.textAlignment
|
||||
textView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||
textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor
|
||||
textView.font = model.fontStyle.getFont()
|
||||
textView.placeholder = model.placeholder ?? ""
|
||||
textView.placeholderFontStyle = model.placeholderFontStyle
|
||||
textView.placeholderTextColor = model.placeholderTextColor.uiColor
|
||||
textView.setPlaceholderIfAvailable()
|
||||
|
||||
switch model.type {
|
||||
case .secure, .password:
|
||||
switch viewModel.type {
|
||||
case .secure:
|
||||
textView.isSecureTextEntry = true
|
||||
textView.shouldMaskWhileRecording = true
|
||||
|
||||
case .numberSecure:
|
||||
textView.isSecureTextEntry = true
|
||||
textView.keyboardType = .numberPad
|
||||
|
||||
case .number:
|
||||
textView.shouldMaskWhileRecording = true
|
||||
textView.keyboardType = .numberPad
|
||||
|
||||
case .email:
|
||||
textView.keyboardType = .emailAddress
|
||||
|
||||
default: break
|
||||
case .securityCode, .creditCard, .password:
|
||||
textView.shouldMaskWhileRecording = true
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Override the preset keyboard set in type.
|
||||
if let keyboardType = viewModel.assignKeyboardType() {
|
||||
textView.keyboardType = keyboardType
|
||||
}
|
||||
|
||||
/// append any internal rules:
|
||||
viewModel.rules = rules
|
||||
|
||||
/// No point in configuring if the TextView is Read-only.
|
||||
if textView.isEditable {
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
setupTextViewToolbar()
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if isSelected {
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.textView.becomeFirstResponder()
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if model.hideBorders {
|
||||
adjustMarginConstraints(constant: 0)
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
updateAccessibility(model: model)
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
text = ""
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateAccessibility(model: TextViewEntryFieldModel) {
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
var message = ""
|
||||
|
||||
if let titleText = model.accessibilityText ?? model.title {
|
||||
message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
}
|
||||
|
||||
if let feedback = model.feedback {
|
||||
message += ", " + feedback
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
}
|
||||
|
||||
extension VDS.TextView: ViewMaskingProtocol {
|
||||
public var shouldMaskWhileRecording: Bool {
|
||||
get {
|
||||
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||
}
|
||||
|
||||
if let errorMessage = errorLabel.text {
|
||||
message += ", " + errorMessage
|
||||
set {
|
||||
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
|
||||
textView.accessibilityLabel = message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
|
||||
class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
public class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
@ -17,24 +17,20 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
public override class var identifier: String { "textView" }
|
||||
|
||||
public var accessibilityText: String?
|
||||
public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge
|
||||
public var height: CGFloat?
|
||||
public var placeholderTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro
|
||||
public var editable: Bool = true
|
||||
public var showsPlaceholder: Bool = false
|
||||
|
||||
public var minHeight: VDS.TextArea.Height = .twoX
|
||||
public var maxLength: Int?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case accessibilityText
|
||||
case fontStyle
|
||||
case height
|
||||
case placeholderFontStyle
|
||||
case placeholderTextColor
|
||||
case editable
|
||||
case minHeight
|
||||
case maxLength
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -45,34 +41,18 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
try super.init(from: decoder)
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let placeholderFontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .placeholderFontStyle) {
|
||||
self.placeholderFontStyle = placeholderFontStyle
|
||||
}
|
||||
|
||||
if let placeholderTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .placeholderTextColor) {
|
||||
self.placeholderTextColor = placeholderTextColor
|
||||
}
|
||||
|
||||
if let fontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .fontStyle) {
|
||||
self.fontStyle = fontStyle
|
||||
}
|
||||
|
||||
if let editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) {
|
||||
self.editable = editable
|
||||
}
|
||||
|
||||
editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) ?? true
|
||||
minHeight = try typeContainer.decodeIfPresent(VDS.TextArea.Height.self, forKey: .minHeight) ?? .twoX
|
||||
maxLength = try typeContainer.decodeIfPresent(Int.self, forKey: .maxLength)
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(height, forKey: .height)
|
||||
try container.encode(fontStyle, forKey: .fontStyle)
|
||||
try container.encode(editable, forKey: .editable)
|
||||
try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle)
|
||||
try container.encode(placeholderTextColor, forKey: .placeholderTextColor)
|
||||
try container.encode(minHeight, forKey: .minHeight)
|
||||
try container.encodeIfPresent(maxLength, forKey: .maxLength)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,243 +5,64 @@
|
||||
// Created by Scott Pfeil on 4/9/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
open class RadioBox: Control, MFButtonProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class RadioBox: VDS.RadioBoxItem, VDSMoleculeViewProtocol, MFButtonProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioBoxModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open var isOutOfStock: Bool {
|
||||
get { strikethrough }
|
||||
set {
|
||||
strikethrough = newValue
|
||||
viewModel?.strikethrough = newValue
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public let label = Label(fontStyle: .RegularBodySmall)
|
||||
public let subTextLabel = Label(fontStyle: .RegularMicro)
|
||||
public var isOutOfStock = false
|
||||
public var accentColor = UIColor.mvmRed
|
||||
public func viewModelDidUpdate() {
|
||||
|
||||
public let innerPadding: CGFloat = 12.0
|
||||
|
||||
private var borderLayer: CALayer?
|
||||
private var strikeLayer: CALayer?
|
||||
private var maskLayer: CALayer?
|
||||
|
||||
public var subTextLabelHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
var additionalData: [AnyHashable: Any]?
|
||||
|
||||
public var radioBoxModel: RadioBoxModel? {
|
||||
model as? RadioBoxModel
|
||||
}
|
||||
|
||||
public override var isSelected: Bool {
|
||||
didSet { updateAccessibility() }
|
||||
}
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
didSet { updateAccessibility() }
|
||||
}
|
||||
text = viewModel.text
|
||||
subText = viewModel.subText
|
||||
subTextRight = viewModel.subTextRight
|
||||
strikethrough = viewModel.strikethrough
|
||||
isSelected = viewModel.selected
|
||||
isEnabled = viewModel.enabled && !viewModel.readOnly
|
||||
|
||||
onChange = { [weak self] _ in
|
||||
if let radioBoxModel = self?.viewModel, let actionModel = radioBoxModel.action {
|
||||
Task(priority: .userInitiated) { [weak self] in
|
||||
guard let self else { return }
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioBoxModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Functions
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc open func selectBox() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
@objc open func deselectBox() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
label.updateView(size)
|
||||
subTextLabel.updateView(size)
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
layer.delegate = self
|
||||
layer.borderColor = UIColor.mvmCoolGray6.cgColor
|
||||
layer.borderWidth = 1
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
label.numberOfLines = 1
|
||||
addSubview(label)
|
||||
NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: innerPadding, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
|
||||
|
||||
subTextLabel.textColor = .mvmCoolGray6
|
||||
subTextLabel.numberOfLines = 1
|
||||
addSubview(subTextLabel)
|
||||
NSLayoutConstraint.constraintPinSubview(subTextLabel, pinTop: false, topConstant:0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: subTextLabel.bottomAnchor, constant: innerPadding).isActive = true
|
||||
subTextLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 2).isActive = true
|
||||
subTextLabelHeightConstraint = subTextLabel.heightAnchor.constraint(equalToConstant: 0)
|
||||
subTextLabelHeightConstraint?.isActive = true
|
||||
|
||||
addTarget(self, action: #selector(selectBox), for: .touchUpInside)
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? RadioBoxModel else { return }
|
||||
self.delegateObject = delegateObject
|
||||
self.additionalData = additionalData
|
||||
label.text = model.text
|
||||
subTextLabel.text = model.subText
|
||||
isOutOfStock = model.strikethrough
|
||||
subTextLabelHeightConstraint?.isActive = (subTextLabel.text?.count ?? 0) == 0
|
||||
if let color = model.selectedAccentColor?.uiColor {
|
||||
accentColor = color
|
||||
}
|
||||
isSelected = model.selected
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
backgroundColor = .white
|
||||
accentColor = .mvmRed
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - State Handling
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func draw(_ layer: CALayer, in ctx: CGContext) {
|
||||
// Draw the strikethrough
|
||||
strikeLayer?.removeFromSuperlayer()
|
||||
if isOutOfStock {
|
||||
let line = getStrikeThrough(color: isSelected ? .black : .mvmCoolGray6, thickness: 1)
|
||||
layer.addSublayer(line)
|
||||
strikeLayer = line
|
||||
}
|
||||
|
||||
// Draw the border
|
||||
borderLayer?.removeFromSuperlayer()
|
||||
if isSelected {
|
||||
layer.borderWidth = 0
|
||||
let border = getSelectedBorder()
|
||||
layer.addSublayer(border)
|
||||
borderLayer = border
|
||||
} else {
|
||||
layer.borderWidth = 1
|
||||
}
|
||||
|
||||
// Handle Mask
|
||||
maskLayer?.removeFromSuperlayer()
|
||||
if !isEnabled {
|
||||
let mask = getMaskLayer()
|
||||
layer.mask = mask
|
||||
maskLayer = mask
|
||||
}
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Accounts for any size changes
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
@objc open func selectBox() {
|
||||
guard isEnabled, !isSelected else { return }
|
||||
isSelected = true
|
||||
radioBoxModel?.selected = isSelected
|
||||
if let radioBoxModel = radioBoxModel, let actionModel = radioBoxModel.action {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioBoxModel)
|
||||
}
|
||||
}
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
@objc open func deselectBox() {
|
||||
isSelected = false
|
||||
radioBoxModel?.selected = isSelected
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
/// Gets the selected state border
|
||||
func getSelectedBorder() -> CAShapeLayer {
|
||||
let layer = CAShapeLayer()
|
||||
|
||||
let topLineWidth: CGFloat = 4
|
||||
let topLinePath = UIBezierPath()
|
||||
topLinePath.lineWidth = topLineWidth
|
||||
topLinePath.move(to: CGPoint(x: 0, y: topLineWidth / 2.0))
|
||||
topLinePath.addLine(to: CGPoint(x: bounds.width, y: topLineWidth / 2.0))
|
||||
|
||||
let topLineLayer = CAShapeLayer()
|
||||
topLineLayer.fillColor = nil
|
||||
topLineLayer.strokeColor = accentColor.cgColor
|
||||
topLineLayer.lineWidth = 4
|
||||
topLineLayer.path = topLinePath.cgPath
|
||||
layer.addSublayer(topLineLayer)
|
||||
|
||||
let lineWidth: CGFloat = 1
|
||||
let halfLineWidth: CGFloat = 0.5
|
||||
let linePath = UIBezierPath()
|
||||
linePath.move(to: CGPoint(x: halfLineWidth, y: topLineWidth))
|
||||
linePath.addLine(to: CGPoint(x: halfLineWidth, y: bounds.height))
|
||||
linePath.move(to: CGPoint(x: 0, y: bounds.height - halfLineWidth))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - halfLineWidth))
|
||||
linePath.move(to: CGPoint(x: bounds.width - halfLineWidth, y: bounds.height))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width - halfLineWidth, y: topLineWidth))
|
||||
|
||||
let borderLayer = CAShapeLayer()
|
||||
borderLayer.fillColor = nil
|
||||
borderLayer.strokeColor = UIColor.black.cgColor
|
||||
borderLayer.lineWidth = lineWidth
|
||||
borderLayer.path = linePath.cgPath
|
||||
layer.addSublayer(borderLayer)
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
/// Adds a border to edge
|
||||
func getStrikeThrough(color: UIColor, thickness: CGFloat) -> CAShapeLayer {
|
||||
let border = CAShapeLayer()
|
||||
border.name = "strikethrough"
|
||||
border.fillColor = nil
|
||||
border.opacity = 1.0
|
||||
border.lineWidth = thickness
|
||||
border.strokeColor = color.cgColor
|
||||
|
||||
let linePath = UIBezierPath()
|
||||
linePath.move(to: CGPoint(x: 0, y: bounds.height))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width, y: 0))
|
||||
border.path = linePath.cgPath
|
||||
return border
|
||||
}
|
||||
|
||||
func getMaskLayer() -> CALayer {
|
||||
let mask = CALayer()
|
||||
mask.backgroundColor = UIColor.white.cgColor
|
||||
mask.opacity = 0.3
|
||||
mask.frame = bounds
|
||||
return mask
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
|
||||
public func updateAccessibility() {
|
||||
|
||||
var message = ""
|
||||
|
||||
if let labelText = label.text, label.hasText {
|
||||
message += labelText + ", "
|
||||
}
|
||||
|
||||
if let subLabelText = subTextLabel.text, subTextLabel.hasText {
|
||||
message += subLabelText + ", "
|
||||
}
|
||||
|
||||
accessibilityLabel = message
|
||||
accessibilityTraits = .button
|
||||
|
||||
if isSelected {
|
||||
accessibilityTraits.insert(.selected)
|
||||
}
|
||||
|
||||
if !isEnabled {
|
||||
accessibilityTraits.insert(.notEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
//--------------------------------------------------
|
||||
@ -17,15 +18,17 @@ import MVMCore
|
||||
|
||||
public var text: String
|
||||
public var subText: String?
|
||||
public var subTextRight: String?
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var selectedAccentColor: Color?
|
||||
public var selected: Bool = false
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var strikethrough: Bool = false
|
||||
public var fieldValue: String?
|
||||
public var action: ActionModelProtocol?
|
||||
public var inverted: Bool = false
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
@ -36,7 +39,7 @@ import MVMCore
|
||||
case moleculeName
|
||||
case text
|
||||
case subText
|
||||
case selectedAccentColor
|
||||
case subTextRight
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case selected
|
||||
@ -45,6 +48,7 @@ import MVMCore
|
||||
case fieldValue
|
||||
case action
|
||||
case readOnly
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -65,8 +69,7 @@ import MVMCore
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
text = try typeContainer.decode(String.self, forKey: .text)
|
||||
subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText)
|
||||
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
subTextRight = try typeContainer.decodeIfPresent(String.self, forKey: .subTextRight)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) {
|
||||
@ -80,6 +83,10 @@ import MVMCore
|
||||
strikethrough = isStrikeTrough
|
||||
}
|
||||
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
|
||||
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
}
|
||||
@ -90,8 +97,7 @@ import MVMCore
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encodeIfPresent(subText, forKey: .subText)
|
||||
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(subTextRight, forKey: .subTextRight)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(selected, forKey: .selected)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
@ -99,5 +105,6 @@ import MVMCore
|
||||
try container.encode(strikethrough, forKey: .strikethrough)
|
||||
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,172 +7,69 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
public protocol RadioBoxSelectionDelegate: AnyObject {
|
||||
func selected(radioBox: RadioBoxModel)
|
||||
}
|
||||
|
||||
open class RadioBoxes: View {
|
||||
|
||||
public var collectionView: CollectionView!
|
||||
public var collectionViewHeight: NSLayoutConstraint!
|
||||
private let boxWidth: CGFloat = 151.0
|
||||
private let boxHeight: CGFloat = 64.0
|
||||
private var itemSpacing: CGFloat = 12.0
|
||||
private var numberOfColumns: CGFloat = 2.0
|
||||
private var radioBoxesModel: RadioBoxesModel? {
|
||||
return model as? RadioBoxesModel
|
||||
}
|
||||
public weak var radioDelegate: RadioBoxSelectionDelegate?
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioBoxesModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
/// The models for the molecules.
|
||||
public var boxes: [RadioBoxModel]?
|
||||
public weak var radioDelegate: RadioBoxSelectionDelegate?
|
||||
|
||||
private var size: CGFloat?
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Accounts for any collection size changes
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
// TODO: this matches the current accessibility however not what was passed by Barbara's team.
|
||||
// open override var items: [RadioBoxItem] {
|
||||
// didSet {
|
||||
// let total = items.count
|
||||
// for (index, radioBoxItem) in items.enumerated() {
|
||||
// radioBoxItem.selectorView.bridge_accessibilityValueBlock = {
|
||||
// guard let format = MVMCoreUIUtility.hardcodedString(withKey: "index_string_of_total"),
|
||||
// let indexString = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: index + 1)) else { return ""}
|
||||
// return String(format: format, indexString, total)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
}
|
||||
|
||||
open func updateAccessibilityValue(collectionView: UICollectionView, cell: RadioBoxCollectionViewCell, indexPath: IndexPath) {
|
||||
guard let format = MVMCoreUIUtility.hardcodedString(withKey: "index_string_of_total"),
|
||||
let indexString = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: indexPath.row + 1)) else { return }
|
||||
let total = self.collectionView(collectionView, numberOfItemsInSection: indexPath.section)
|
||||
cell.accessibilityValue = String(format: format, indexString, total)
|
||||
}
|
||||
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
collectionView = createCollectionView()
|
||||
addSubview(collectionView)
|
||||
NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)
|
||||
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
|
||||
collectionViewHeight?.isActive = true
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MoleculeViewProtocol
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
public func viewModelDidUpdate() {
|
||||
boxes = viewModel.boxes
|
||||
surface = viewModel.surface
|
||||
selectorModels = viewModel.selectorModels
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
guard let model = model as? RadioBoxesModel else { return }
|
||||
boxes = model.boxes
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open override func didSelect(_ selectedControl: RadioBoxItem) {
|
||||
super.didSelect(selectedControl)
|
||||
|
||||
backgroundColor = model.backgroundColor?.uiColor
|
||||
|
||||
registerCells()
|
||||
setHeight()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
@objc override open func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
self.size = size
|
||||
itemSpacing = Padding.Component.gutterFor(size: size)
|
||||
collectionView.updateView(size)
|
||||
}
|
||||
|
||||
// MARK: - Creation
|
||||
|
||||
/// Creates the layout for the collection.
|
||||
open func createCollectionViewLayout() -> UICollectionViewLayout {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .vertical
|
||||
layout.minimumLineSpacing = itemSpacing
|
||||
layout.minimumInteritemSpacing = itemSpacing
|
||||
return layout
|
||||
}
|
||||
|
||||
/// Creates the collection view.
|
||||
open func createCollectionView() -> CollectionView {
|
||||
let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
|
||||
collection.dataSource = self
|
||||
collection.delegate = self
|
||||
return collection
|
||||
}
|
||||
|
||||
/// Registers the cells with the collection view
|
||||
open func registerCells() {
|
||||
collectionView.register(RadioBoxCollectionViewCell.self, forCellWithReuseIdentifier: "RadioBoxCollectionViewCell")
|
||||
}
|
||||
|
||||
// MARK: - JSON Setters
|
||||
open func setHeight() {
|
||||
guard let boxes = boxes, boxes.count > 0 else {
|
||||
collectionViewHeight.constant = 0
|
||||
return
|
||||
// since the boxes has the state being tracked, we need to update the values here.
|
||||
if let index = items.firstIndex(where: {$0 === selectedControl}), let selectedBox = boxes?[index] {
|
||||
boxes?.forEach {$0.selected = false }
|
||||
selectedBox.selected = true
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
radioDelegate?.selected(radioBox: selectedBox)
|
||||
}
|
||||
|
||||
// Calculate the height
|
||||
let rows = ceil(CGFloat(boxes.count) / numberOfColumns)
|
||||
let height = (rows * boxHeight) + ((rows - 1) * itemSpacing)
|
||||
collectionViewHeight?.constant = height
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDelegateFlowLayout {
|
||||
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let itemWidth: CGFloat = (collectionView.bounds.width - itemSpacing) / numberOfColumns
|
||||
return CGSize(width: itemWidth, height: boxHeight)
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDataSource {
|
||||
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return boxes?.count ?? 0
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let molecule = boxes?[indexPath.row],
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioBoxCollectionViewCell", for: indexPath) as? RadioBoxCollectionViewCell else {
|
||||
fatalError()
|
||||
}
|
||||
cell.reset()
|
||||
cell.radioBox.isUserInteractionEnabled = false
|
||||
|
||||
if let color = radioBoxesModel?.boxesColor {
|
||||
cell.radioBox.backgroundColor = color.uiColor
|
||||
}
|
||||
if let color = radioBoxesModel?.selectedAccentColor {
|
||||
cell.radioBox.accentColor = color.uiColor
|
||||
}
|
||||
|
||||
cell.set(with: molecule, delegateObject, nil)
|
||||
cell.updateView(size ?? collectionView.bounds.width)
|
||||
if molecule.selected {
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
updateAccessibilityValue(collectionView: collectionView, cell: cell, indexPath: indexPath)
|
||||
cell.layoutIfNeeded()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDelegate {
|
||||
open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
guard let molecule = boxes?[indexPath.row] else { return false }
|
||||
return molecule.enabled
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
|
||||
cell.radioBox.selectBox()
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
cell.updateAccessibility()
|
||||
guard let radioBox = boxes?[indexPath.row] else { return }
|
||||
radioDelegate?.selected(radioBox: radioBox)
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
|
||||
cell.radioBox.deselectBox()
|
||||
cell.updateAccessibility()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,32 +6,36 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
@objcMembers public class RadioBoxesModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "radioBoxes"
|
||||
public var id: String = UUID().uuidString
|
||||
public override static var identifier: String { "radioBoxes" }
|
||||
|
||||
public var boxes: [RadioBoxModel]
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var selectedAccentColor: Color?
|
||||
public var boxesColor: Color?
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
|
||||
|
||||
public var selectorModels: [VDS.RadioBoxGroup.RadioBoxItemModel] {
|
||||
boxes.compactMap({ item in
|
||||
var radioBox = RadioBoxGroup.RadioBoxItemModel()
|
||||
radioBox.text = item.text
|
||||
radioBox.subText = item.subText
|
||||
radioBox.subTextRight = item.subTextRight
|
||||
radioBox.surface = surface
|
||||
radioBox.selected = item.selected
|
||||
radioBox.strikethrough = item.strikethrough
|
||||
radioBox.disabled = !(item.enabled && !item.readOnly)
|
||||
return radioBox
|
||||
})
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the fieldValue of the selected box, otherwise the text of the selected box.
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
let selectedBox = boxes.first { (box) -> Bool in
|
||||
return box.selected
|
||||
@ -42,7 +46,7 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
open override func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
@ -51,17 +55,7 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case enabled
|
||||
case readOnly
|
||||
case selectedAccentColor
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case boxesColor
|
||||
case boxes
|
||||
case fieldKey
|
||||
case groupName
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -69,7 +63,8 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with boxes: [RadioBoxModel]){
|
||||
self.boxes = boxes
|
||||
self.boxes = boxes
|
||||
super.init()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -78,32 +73,13 @@ import MVMCore
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
boxesColor = try typeContainer.decodeIfPresent(Color.self, forKey: .boxesColor)
|
||||
boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes)
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
baseValue = formFieldValue()
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(boxes, forKey: .boxes)
|
||||
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encode(groupName, forKey: .groupName)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,37 +7,35 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import VDS
|
||||
|
||||
@objcMembers open class RadioButton: Control, MFButtonProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class RadioButton: VDS.RadioButton, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol, MVMCoreUIViewConstrainingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var diameter: CGFloat = 20 {
|
||||
didSet { widthConstraint?.constant = diameter }
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioButtonModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open var radioButtonModel: RadioButtonModel {
|
||||
viewModel
|
||||
}
|
||||
|
||||
@objc public override var isSelected: Bool {
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
open override var isSelected: Bool {
|
||||
didSet {
|
||||
radioModel?.state = isSelected
|
||||
updateAccessibilityLabel()
|
||||
viewModel.state = isSelected
|
||||
if oldValue != isSelected {
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
public var enabledColor: UIColor {
|
||||
return radioModel?.inverted ?? false ? VDSColor.elementsPrimaryOndark : VDSColor.elementsPrimaryOnlight
|
||||
}
|
||||
public var disabledColor: UIColor {
|
||||
return radioModel?.inverted ?? false ? VDSColor.interactiveDisabledOndark : VDSColor.interactiveDisabledOnlight
|
||||
}
|
||||
public var delegateObject: MVMCoreUIDelegateObject?
|
||||
var additionalData: [AnyHashable: Any]?
|
||||
|
||||
public var radioModel: RadioButtonModel? {
|
||||
model as? RadioButtonModel
|
||||
}
|
||||
|
||||
lazy public var radioGroupName: String? = { radioModel?.fieldKey }()
|
||||
|
||||
lazy public var radioGroupName: String? = { viewModel.fieldKey }()
|
||||
|
||||
lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
|
||||
|
||||
@ -48,132 +46,108 @@ import VDSCoreTokens
|
||||
return radioButtonModel
|
||||
}()
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
didSet {
|
||||
isUserInteractionEnabled = isEnabled
|
||||
setNeedsDisplay()
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
public var widthConstraint: NSLayoutConstraint?
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
let color = isEnabled == false ? disabledColor.cgColor : enabledColor.cgColor
|
||||
layer.cornerRadius = bounds.width * 0.5
|
||||
layer.borderColor = color
|
||||
layer.borderWidth = bounds.width * 0.0333
|
||||
|
||||
if isSelected {
|
||||
// Space around inner circle is 1/5 the size
|
||||
context.addEllipse(in: CGRect(x: bounds.width * 0.2,
|
||||
y: bounds.height * 0.2,
|
||||
width: bounds.width * 0.6,
|
||||
height: bounds.height * 0.6))
|
||||
context.setFillColor(color)
|
||||
context.fillPath()
|
||||
}
|
||||
/// There is currently no intention on using xib files.
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
fatalError("xib file is not implemented for Checkbox.")
|
||||
}
|
||||
|
||||
public convenience required init() {
|
||||
self.init(frame:.zero)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The action performed when tapped.
|
||||
func tapAction() {
|
||||
if !isEnabled {
|
||||
return
|
||||
}
|
||||
public func isValidField() -> Bool { isSelected }
|
||||
|
||||
public func formFieldName() -> String? {
|
||||
viewModel.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldGroupName() -> String? {
|
||||
viewModel.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
guard let radioModel = viewModel, radioModel.enabled else { return nil }
|
||||
return radioModel.fieldValue
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
|
||||
//events
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let isValid = viewModel.isValid ?? true
|
||||
showError = !isValid
|
||||
isEnabled = viewModel.enabled
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
isSelected = viewModel.state
|
||||
isEnabled = viewModel.enabled && !viewModel.readOnly
|
||||
RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel, self, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
open override func toggle() {
|
||||
guard !isSelected, isEnabled else { return }
|
||||
|
||||
//removed error
|
||||
if showError && isSelected == false {
|
||||
showError.toggle()
|
||||
}
|
||||
|
||||
let wasPreviouslySelected = isSelected
|
||||
if let radioButtonModel = radioButtonSelectionHelper {
|
||||
radioButtonModel.selected(self)
|
||||
if let radioButtonSelectionHelper {
|
||||
radioButtonSelectionHelper.selected(self)
|
||||
} else {
|
||||
isSelected = !isSelected
|
||||
}
|
||||
|
||||
if let radioModel = radioModel, let actionModel = radioModel.action, isSelected, !wasPreviouslySelected {
|
||||
if let actionModel = viewModel.action, isSelected, !wasPreviouslySelected {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioModel)
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func isValidField() -> Bool { isSelected }
|
||||
|
||||
public func formFieldName() -> String? {
|
||||
radioModel?.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldGroupName() -> String? {
|
||||
radioModel?.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
guard let radioModel = radioModel, radioModel.enabled else { return nil }
|
||||
return radioModel.fieldValue
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Adjust accessibility label based on state of RadioButton.
|
||||
func updateAccessibilityLabel() {
|
||||
if let message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button"),
|
||||
let selectedState = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "radio_selected_state" : "radio_not_selected_state") {
|
||||
accessibilityLabel = message + selectedState
|
||||
}
|
||||
/// This will toggle the state of the Checkbox and execute the actionBlock if provided.
|
||||
public func tapAction() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMViewProtocol
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
open func needsToBeConstrained() -> Bool { true }
|
||||
|
||||
backgroundColor = .clear
|
||||
clipsToBounds = true
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 20)
|
||||
widthConstraint?.isActive = true
|
||||
heightConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1)
|
||||
heightConstraint?.isActive = true
|
||||
|
||||
addTarget(self, action: #selector(tapAction), for: .touchUpInside)
|
||||
isAccessibilityElement = true
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "radio_action_hint")
|
||||
accessibilityTraits = .button
|
||||
updateAccessibilityLabel()
|
||||
}
|
||||
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
self.additionalData = additionalData
|
||||
|
||||
guard let model = model as? RadioButtonModel else { return }
|
||||
|
||||
isSelected = model.state
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
RadioButtonSelectionHelper.setupForRadioButtonGroup(model, self, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,48 +7,29 @@
|
||||
//
|
||||
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
|
||||
open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
open class RadioButtonModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "radioButton"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public static override var identifier: String { "radioButton" }
|
||||
public var state: Bool = false
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
|
||||
/// The specific value to send to server. TODO: update this to be more generic.
|
||||
public var fieldValue: String?
|
||||
|
||||
public var baseValue: AnyHashable?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var fieldKey: String?
|
||||
public var action: ActionModelProtocol?
|
||||
public var inverted: Bool = false
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case state
|
||||
case enabled
|
||||
case fieldValue
|
||||
case fieldKey
|
||||
case groupName
|
||||
case action
|
||||
case readOnly
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -56,69 +37,50 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(_ state: Bool) {
|
||||
super.init()
|
||||
self.state = state
|
||||
baseValue = state
|
||||
self.baseValue = state
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
return fieldValue
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
open override func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
}
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
baseValue = state
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(state, forKey: .state)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeIfPresent(inverted, forKey: .inverted)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
public protocol RadioButtonSelectionHelperProtocol: AnyObject {
|
||||
var isSelected: Bool { get set }
|
||||
var radioButtonModel: RadioButtonModel { get }
|
||||
}
|
||||
|
||||
@objcMembers public class RadioButtonSelectionHelper: FormFieldProtocol {
|
||||
//--------------------------------------------------
|
||||
@ -14,7 +18,7 @@
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
private var selectedRadioButton: RadioButton?
|
||||
private var selectedRadioButton: RadioButtonSelectionHelperProtocol?
|
||||
private var selectedRadioButtonModel: RadioButtonModel?
|
||||
public var baseValue: AnyHashable?
|
||||
public var enabled: Bool = true
|
||||
@ -24,7 +28,7 @@
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) {
|
||||
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol) {
|
||||
self.fieldKey = radioButtonModel.fieldKey
|
||||
self.groupName = radioButtonModel.groupName
|
||||
|
||||
@ -49,7 +53,7 @@
|
||||
// MARK: - Functions
|
||||
//--------------------------------------------------
|
||||
|
||||
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
|
||||
guard let groupName = radioButtonModel.fieldKey,
|
||||
let formValidator = delegateObject?.formHolderDelegate?.formValidator
|
||||
@ -61,10 +65,10 @@
|
||||
FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
public func selected(_ radioButton: RadioButton) {
|
||||
public func selected(_ radioButton: RadioButtonSelectionHelperProtocol) {
|
||||
|
||||
// Checks because the view could be reused
|
||||
if selectedRadioButton?.radioModel === selectedRadioButtonModel {
|
||||
if selectedRadioButton?.radioButtonModel === selectedRadioButtonModel {
|
||||
selectedRadioButton?.isSelected = false
|
||||
} else {
|
||||
selectedRadioButtonModel?.state = false
|
||||
@ -72,7 +76,7 @@
|
||||
|
||||
selectedRadioButton = radioButton
|
||||
selectedRadioButton?.isSelected = true
|
||||
selectedRadioButtonModel = selectedRadioButton?.radioModel
|
||||
selectedRadioButtonModel = selectedRadioButton?.radioButtonModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
import MVMCore
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
|
||||
@ -19,137 +20,40 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
Container: The background of the toggle control.
|
||||
Knob: The circular indicator that slides on the container.
|
||||
*/
|
||||
@objcMembers open class Toggle: Control, MVMCoreUIViewConstrainingProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class Toggle: VDS.Toggle, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
//------------------------------------------------------
|
||||
open var viewModel: ToggleModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
/// Holds the on and off colors for the container.
|
||||
public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack)
|
||||
|
||||
/// Holds the on and off colors for the knob.
|
||||
public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite)
|
||||
|
||||
/// Holds the on and off colors for the disabled state..
|
||||
public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite)
|
||||
|
||||
/// Set this flag to false if you do not want to animate state changes.
|
||||
public var isAnimated = true
|
||||
|
||||
public var didToggleAction: ActionBlock?
|
||||
public var didToggleAction: ActionBlock? {
|
||||
didSet {
|
||||
if let didToggleAction {
|
||||
onChange = { _ in
|
||||
didToggleAction()
|
||||
}
|
||||
} else {
|
||||
onChange = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute.
|
||||
public var shouldToggleAction: ActionBlockConfirmation? = {
|
||||
return { true }
|
||||
}()
|
||||
|
||||
// Sizes are from InVision design specs.
|
||||
static let containerSize = CGSize(width: 51, height: 31)
|
||||
static let knobSize = CGSize(width: 28, height: 28)
|
||||
|
||||
private var knobView: View = {
|
||||
let view = View()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = Toggle.getKnobHeight() / 2.0
|
||||
return view
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
open override var isEnabled: Bool {
|
||||
didSet {
|
||||
isUserInteractionEnabled = isEnabled
|
||||
changeStateNoAnimation(isEnabled ? isOn : false)
|
||||
setToggleAppearanceFromState()
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled")
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple means to prevent user interaction with the toggle.
|
||||
public var isLocked: Bool = false {
|
||||
didSet { isUserInteractionEnabled = !isLocked }
|
||||
}
|
||||
|
||||
/// The state on the toggle. Default value: false.
|
||||
open var isOn: Bool = false {
|
||||
didSet {
|
||||
if isAnimated {
|
||||
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
|
||||
if self.isOn {
|
||||
self.knobView.backgroundColor = self.knobTintColor.on
|
||||
self.backgroundColor = self.containerTintColor.on
|
||||
|
||||
} else {
|
||||
self.knobView.backgroundColor = self.knobTintColor.off
|
||||
self.backgroundColor = self.containerTintColor.off
|
||||
}
|
||||
}, completion: nil)
|
||||
|
||||
UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: {
|
||||
self.constrainKnob()
|
||||
self.knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
self.layoutIfNeeded()
|
||||
}, completion: nil)
|
||||
|
||||
} else {
|
||||
setToggleAppearanceFromState()
|
||||
self.constrainKnob()
|
||||
}
|
||||
|
||||
toggleModel?.selected = isOn
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
accessibilityValue = isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff")
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
public var toggleModel: ToggleModel? {
|
||||
model as? ToggleModel
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
private var knobLeadingConstraint: NSLayoutConstraint?
|
||||
private var knobTrailingConstraint: NSLayoutConstraint?
|
||||
private var knobHeightConstraint: NSLayoutConstraint?
|
||||
private var knobWidthConstraint: NSLayoutConstraint?
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
private var widthConstraint: NSLayoutConstraint?
|
||||
|
||||
private func constrainKnob() {
|
||||
|
||||
knobLeadingConstraint?.isActive = false
|
||||
knobTrailingConstraint?.isActive = false
|
||||
|
||||
_ = isOn ? constrainKnobOn() : constrainKnobOff()
|
||||
|
||||
knobTrailingConstraint?.isActive = true
|
||||
knobLeadingConstraint?.isActive = true
|
||||
}
|
||||
|
||||
private func constrainKnobOn() {
|
||||
|
||||
knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2)
|
||||
knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor)
|
||||
}
|
||||
|
||||
private func constrainKnobOff() {
|
||||
|
||||
knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor)
|
||||
knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -158,7 +62,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
public convenience override init() {
|
||||
public convenience required init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@ -171,7 +75,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
/// - parameter didToggleAction: A closure which is executed after the toggle changes states.
|
||||
public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) {
|
||||
self.init(frame: .zero)
|
||||
changeStateNoAnimation(isOn)
|
||||
self.isOn = isOn
|
||||
self.didToggleAction = didToggleAction
|
||||
}
|
||||
|
||||
@ -191,223 +95,75 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
|
||||
heightConstraint?.constant = Self.getContainerHeight()
|
||||
widthConstraint?.constant = Self.getContainerWidth()
|
||||
|
||||
knobHeightConstraint?.constant = Self.getKnobHeight()
|
||||
knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
|
||||
layer.cornerRadius = Self.getContainerHeight() / 2.0
|
||||
knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0
|
||||
|
||||
changeStateNoAnimation(isOn)
|
||||
}
|
||||
|
||||
public override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint")
|
||||
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
|
||||
accessibilityTraits = .button
|
||||
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height)
|
||||
heightConstraint?.isActive = true
|
||||
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width)
|
||||
widthConstraint?.isActive = true
|
||||
|
||||
layer.cornerRadius = Self.getContainerHeight() / 2.0
|
||||
backgroundColor = containerTintColor.off
|
||||
|
||||
addSubview(knobView)
|
||||
|
||||
knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height)
|
||||
knobHeightConstraint?.isActive = true
|
||||
knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width)
|
||||
knobWidthConstraint?.isActive = true
|
||||
knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
|
||||
knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true
|
||||
|
||||
constrainKnobOff()
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
|
||||
backgroundColor = containerTintColor.off
|
||||
knobView.backgroundColor = knobTintColor.off
|
||||
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
|
||||
isAnimated = true
|
||||
didToggleAction = nil
|
||||
shouldToggleAction = { return true }
|
||||
}
|
||||
|
||||
class func getContainerWidth() -> CGFloat {
|
||||
let containerWidth = Self.containerSize.width
|
||||
return (MFSizeObject(standardSize: containerWidth, standardiPadPortraitSize: CGFloat(Self.containerSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerWidth
|
||||
}
|
||||
|
||||
class func getContainerHeight() -> CGFloat {
|
||||
let containerHeight = Self.containerSize.height
|
||||
return (MFSizeObject(standardSize: containerHeight, standardiPadPortraitSize: CGFloat(Self.containerSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerHeight
|
||||
}
|
||||
|
||||
class func getKnobWidth() -> CGFloat {
|
||||
let knobWidth = Self.knobSize.width
|
||||
return (MFSizeObject(standardSize: knobWidth, standardiPadPortraitSize: CGFloat(Self.knobSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobWidth
|
||||
}
|
||||
|
||||
class func getKnobHeight() -> CGFloat {
|
||||
let knobHeight = Self.knobSize.width
|
||||
return (MFSizeObject(standardSize: knobHeight, standardiPadPortraitSize: CGFloat(Self.knobSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobHeight
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
|
||||
super.sendAction(action, to: target, for: event)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
open override func sendActions(for controlEvents: UIControl.Event) {
|
||||
super.sendActions(for: controlEvents)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
/// This will toggle the state of the Toggle and execute the actionBlock if provided.
|
||||
public func toggleAndAction() {
|
||||
public func viewModelDidUpdate() {
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if let result = shouldToggleAction?(), result {
|
||||
isOn.toggle()
|
||||
didToggleAction?()
|
||||
isOn = viewModel.selected
|
||||
surface = viewModel.surface
|
||||
isAnimated = viewModel.animated
|
||||
isEnabled = viewModel.enabled && !viewModel.readOnly
|
||||
showText = viewModel.showText
|
||||
if let onText = viewModel.onText {
|
||||
self.onText = onText
|
||||
}
|
||||
}
|
||||
|
||||
private func changeStateNoAnimation(_ state: Bool) {
|
||||
|
||||
// Hold state in case User wanted isAnimated to remain off.
|
||||
let isAnimatedState = isAnimated
|
||||
|
||||
isAnimated = false
|
||||
isOn = state
|
||||
isAnimated = isAnimatedState
|
||||
}
|
||||
|
||||
override open func accessibilityActivate() -> Bool {
|
||||
// Hold state in case User wanted isAnimated to remain off.
|
||||
guard isUserInteractionEnabled else { return false }
|
||||
let isAnimatedState = isAnimated
|
||||
isAnimated = false
|
||||
sendActions(for: .touchUpInside)
|
||||
isAnimated = isAnimatedState
|
||||
return true
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - UIResponder
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
|
||||
UIView.animate(withDuration: 0.1, animations: {
|
||||
self.knobWidthConstraint?.constant += PaddingOne
|
||||
self.layoutIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
|
||||
knobReformAnimation()
|
||||
|
||||
// Action only occurs of the user lifts up from withing acceptable region of the toggle.
|
||||
guard let coordinates = touches.first?.location(in: self),
|
||||
coordinates.x > -20,
|
||||
coordinates.x < bounds.width + 20,
|
||||
coordinates.y > -20,
|
||||
coordinates.y < bounds.height + 20
|
||||
else { return }
|
||||
|
||||
sendActions(for: .touchUpInside)
|
||||
}
|
||||
|
||||
public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
|
||||
knobReformAnimation()
|
||||
sendActions(for: .touchCancel)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Animations
|
||||
//--------------------------------------------------
|
||||
|
||||
public func setToggleAppearanceFromState() {
|
||||
|
||||
backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container
|
||||
knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob
|
||||
}
|
||||
|
||||
public func knobReformAnimation() {
|
||||
|
||||
if isAnimated {
|
||||
UIView.animate(withDuration: 0.1, animations: {
|
||||
self.knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
self.layoutIfNeeded()
|
||||
}, completion: nil)
|
||||
|
||||
} else {
|
||||
knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
layoutIfNeeded()
|
||||
if let offText = viewModel.offText {
|
||||
self.offText = offText
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- MoleculeViewProtocol
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
textSize = viewModel.textSize
|
||||
textWeight = viewModel.textWeight
|
||||
textPosition = viewModel.textPosition
|
||||
|
||||
guard let model = model as? ToggleModel else { return }
|
||||
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
containerTintColor.on = model.onTintColor.uiColor
|
||||
containerTintColor.off = model.offTintColor.uiColor
|
||||
knobTintColor.on = model.onKnobTintColor.uiColor
|
||||
knobTintColor.off = model.offKnobTintColor.uiColor
|
||||
isOn = model.selected
|
||||
changeStateNoAnimation(isOn)
|
||||
isAnimated = model.animated
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
|
||||
if let accessibileString = model.accessibilityText {
|
||||
if let accessibileString = viewModel.accessibilityText {
|
||||
accessibilityLabel = accessibileString
|
||||
}
|
||||
|
||||
if model.action != nil || model.alternateAction != nil {
|
||||
if viewModel.action != nil || viewModel.alternateAction != nil {
|
||||
didToggleAction = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.isOn {
|
||||
if let action = model.action {
|
||||
if let action = viewModel.action {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
} else {
|
||||
if let action = model.alternateAction ?? model.action {
|
||||
if let action = viewModel.alternateAction ?? viewModel.action {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
/// This will toggle the state of the Toggle and execute the actionBlock if provided.
|
||||
public func toggleAndAction() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
Self.getContainerHeight()
|
||||
open override func toggle() {
|
||||
if let result = shouldToggleAction?(), result {
|
||||
super.toggle()
|
||||
viewModel?.selected = isOn
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK:- MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
public class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return Self().intrinsicContentSize.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
// Created by Scott Pfeil on 1/14/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import VDS
|
||||
|
||||
public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
//--------------------------------------------------
|
||||
@ -24,10 +24,15 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
public var action: ActionModelProtocol?
|
||||
public var alternateAction: ActionModelProtocol?
|
||||
public var accessibilityText: String?
|
||||
public var onTintColor: Color = Color(uiColor: .mvmGreen)
|
||||
public var offTintColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var onKnobTintColor: Color = Color(uiColor: .mvmWhite)
|
||||
public var offKnobTintColor: Color = Color(uiColor: .mvmWhite)
|
||||
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
public var inverted: Bool = false
|
||||
public var showText: Bool = false
|
||||
public var onText: String?
|
||||
public var offText: String?
|
||||
public var textSize: VDS.Toggle.TextSize = .small
|
||||
public var textWeight: VDS.Toggle.TextWeight = .regular
|
||||
public var textPosition: VDS.Toggle.TextPosition = .left
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
@ -49,10 +54,15 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
case accessibilityIdentifier
|
||||
case alternateAction
|
||||
case accessibilityText
|
||||
case onTintColor
|
||||
case offTintColor
|
||||
case onKnobTintColor
|
||||
case offKnobTintColor
|
||||
|
||||
case inverted
|
||||
case showText
|
||||
case onText
|
||||
case offText
|
||||
case textSize
|
||||
case textWeight
|
||||
case textPosition
|
||||
|
||||
case fieldKey
|
||||
case groupName
|
||||
}
|
||||
@ -102,25 +112,8 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let onTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onTintColor) {
|
||||
self.onTintColor = onTintColor
|
||||
}
|
||||
|
||||
if let offTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offTintColor) {
|
||||
self.offTintColor = offTintColor
|
||||
}
|
||||
|
||||
if let onKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onKnobTintColor) {
|
||||
self.onKnobTintColor = onKnobTintColor
|
||||
}
|
||||
|
||||
if let offKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offKnobTintColor) {
|
||||
self.offKnobTintColor = offKnobTintColor
|
||||
}
|
||||
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
|
||||
baseValue = selected
|
||||
@ -130,6 +123,14 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
}
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
|
||||
inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
showText = try typeContainer.decodeIfPresent(Bool.self, forKey: .showText) ?? false
|
||||
onText = try typeContainer.decodeIfPresent(String.self, forKey: .onText)
|
||||
offText = try typeContainer.decodeIfPresent(String.self, forKey: .offText)
|
||||
textSize = try typeContainer.decodeIfPresent(VDS.Toggle.TextSize.self, forKey: .textSize) ?? .small
|
||||
textWeight = try typeContainer.decodeIfPresent(VDS.Toggle.TextWeight.self, forKey: .textWeight) ?? .regular
|
||||
textPosition = try typeContainer.decodeIfPresent(VDS.Toggle.TextPosition.self, forKey: .textPosition) ?? .left
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -143,13 +144,17 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
try container.encode(selected, forKey: .state)
|
||||
try container.encode(animated, forKey: .animated)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(onTintColor, forKey: .onTintColor)
|
||||
try container.encode(onKnobTintColor, forKey: .onKnobTintColor)
|
||||
try container.encode(onKnobTintColor, forKey: .onKnobTintColor)
|
||||
try container.encode(offKnobTintColor, forKey: .offKnobTintColor)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encode(showText, forKey: .showText)
|
||||
try container.encodeIfPresent(onText, forKey: .onText)
|
||||
try container.encodeIfPresent(offText, forKey: .offText)
|
||||
try container.encode(textSize, forKey: .textSize)
|
||||
try container.encode(textWeight, forKey: .textWeight)
|
||||
try container.encode(textPosition, forKey: .textPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,11 +34,7 @@ public typealias ActionBlock = () -> ()
|
||||
|
||||
/// A specific text index to use as a unique marker.
|
||||
public var hero: Int?
|
||||
|
||||
public var getRange: NSRange {
|
||||
NSRange(location: 0, length: text?.count ?? 0)
|
||||
}
|
||||
|
||||
|
||||
public var shouldMaskWhileRecording: Bool = false
|
||||
|
||||
public var hasText: Bool {
|
||||
@ -378,19 +374,24 @@ extension Label {
|
||||
|
||||
// MARK: - Atomization
|
||||
extension Label {
|
||||
|
||||
|
||||
public func needsToBeConstrained() -> Bool { true }
|
||||
|
||||
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
public func copyBackgroundColor() -> Bool { true }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Multi-Link Functionality
|
||||
extension Label {
|
||||
extension VDS.Label {
|
||||
|
||||
public var getRange: NSRange {
|
||||
NSRange(location: 0, length: text?.count ?? 0)
|
||||
}
|
||||
|
||||
/// Underlines the tappable region and stores the tap logic for interation.
|
||||
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
|
||||
internal func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
|
||||
guard let text, text.isValid(range: range) else { return }
|
||||
|
||||
var textLink = ActionLabelAttribute(location: range.location, length: range.length)
|
||||
@ -417,8 +418,16 @@ extension Label {
|
||||
return { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
|
||||
if let button = self as? MFButtonProtocol {
|
||||
if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(button, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap,
|
||||
additionalData: additionalData,
|
||||
delegateObject: delegateObject)
|
||||
}
|
||||
} else {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap,
|
||||
additionalData: additionalData,
|
||||
delegateObject: delegateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,9 +81,7 @@
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -98,9 +98,7 @@ import UIKit
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -85,7 +85,6 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -7,54 +7,121 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioButtonLabel: VDS.RadioButtonItem, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol {
|
||||
|
||||
@objcMembers public class RadioButtonLabel: View {
|
||||
|
||||
public let radioButton = RadioButton()
|
||||
var delegateObject: MVMCoreUIDelegateObject?
|
||||
let label = Label()
|
||||
|
||||
public override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
radioButton.updateView(size)
|
||||
label.updateView(size)
|
||||
}
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioButtonLabelModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
open var radioButtonModel: RadioButtonModel {
|
||||
viewModel.radioButton
|
||||
}
|
||||
|
||||
addSubview(radioButton)
|
||||
radioButton.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: 0).isActive = true
|
||||
radioButton.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: PaddingOne).isActive = true
|
||||
layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: radioButton.bottomAnchor, constant: PaddingOne).isActive = true
|
||||
radioButton.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor).isActive = true
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
if let rightView = createRightView() {
|
||||
addSubview(rightView)
|
||||
rightView.leftAnchor.constraint(equalTo: radioButton.rightAnchor, constant: Padding.Component.gutterForApplicationWidth).isActive = true
|
||||
rightView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: 0).isActive = true
|
||||
|
||||
var constraint = rightView.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: PaddingOne)
|
||||
constraint.priority = .defaultHigh
|
||||
constraint.isActive = true
|
||||
|
||||
constraint = layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: rightView.bottomAnchor, constant: PaddingOne)
|
||||
constraint.priority = .defaultHigh
|
||||
constraint.isActive = true
|
||||
layoutMarginsGuide.centerYAnchor.constraint(equalTo: rightView.centerYAnchor).isActive = true
|
||||
open override var isSelected: Bool {
|
||||
didSet {
|
||||
radioButtonModel.state = isSelected
|
||||
if oldValue != isSelected {
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createRightView() -> Container? {
|
||||
let rightView = Container(andContain: label)
|
||||
return rightView
|
||||
lazy public var radioGroupName: String? = { viewModel.radioButton.fieldKey }()
|
||||
|
||||
lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
|
||||
|
||||
guard let radioGroupName = radioGroupName,
|
||||
let radioButtonModel = delegateObject?.formHolderDelegate?.formValidator?.radioButtonsModelByGroup[radioGroupName]
|
||||
else { return nil }
|
||||
|
||||
return radioButtonModel
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Life Cycle
|
||||
//--------------------------------------------------
|
||||
@objc open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open override func toggle() {
|
||||
guard !isSelected, isEnabled else { return }
|
||||
|
||||
//removed error
|
||||
if showError && isSelected == false {
|
||||
showError.toggle()
|
||||
}
|
||||
|
||||
let wasPreviouslySelected = isSelected
|
||||
if let radioButtonSelectionHelper {
|
||||
radioButtonSelectionHelper.selected(self)
|
||||
} else {
|
||||
isSelected = !isSelected
|
||||
}
|
||||
|
||||
if let actionModel = viewModel.radioButton.action, isSelected, !wasPreviouslySelected {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: viewModel)
|
||||
}
|
||||
}
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
}
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
guard let radioButtonLabelModel = model as? RadioButtonLabelModel else { return }
|
||||
|
||||
radioButton.set(with: radioButtonLabelModel.radioButton, delegateObject, additionalData)
|
||||
label.set(with: radioButtonLabelModel.label, delegateObject, additionalData)
|
||||
//--------------------------------------------------
|
||||
// MARK: - Atomic
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
surface = viewModel.surface
|
||||
|
||||
updateRadioButton()
|
||||
|
||||
//primary label
|
||||
labelText = viewModel.label.text
|
||||
if let attributes = viewModel.label.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
|
||||
labelTextAttributes = attributes
|
||||
}
|
||||
|
||||
//secondary label
|
||||
if let subTitleModel = viewModel.subTitle {
|
||||
childText = subTitleModel.text
|
||||
if let attributes = subTitleModel.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
|
||||
childTextAttributes = attributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateRadioButton() {
|
||||
|
||||
if let fieldKey = viewModel.radioButton.fieldKey {
|
||||
self.fieldKey = fieldKey
|
||||
}
|
||||
|
||||
//properties
|
||||
isEnabled = viewModel.radioButton.enabled && !viewModel.radioButton.readOnly
|
||||
isSelected = viewModel.radioButton.state
|
||||
|
||||
//forms
|
||||
RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel.radioButton, self, delegateObject: delegateObject)
|
||||
|
||||
//events
|
||||
viewModel.radioButton.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let isValid = viewModel.radioButton.isValid ?? true
|
||||
showError = !isValid
|
||||
errorText = viewModel.radioButton.errorMessage
|
||||
isEnabled = viewModel.radioButton.enabled
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,8 +8,9 @@
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol {
|
||||
@objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
@ -21,14 +22,23 @@ import MVMCore
|
||||
public var moleculeName: String = RadioButtonLabelModel.identifier
|
||||
public var radioButton: RadioButtonModel
|
||||
public var label: LabelModel
|
||||
public var subTitle: LabelModel?
|
||||
public var inverted: Bool? = false
|
||||
public var surface: Surface { inverted ?? false ? .dark : .light }
|
||||
|
||||
public var children: [MoleculeModelProtocol] {
|
||||
guard let subTitle else { return [radioButton, label] }
|
||||
return [radioButton, label, subTitle]
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(radioButton: RadioButtonModel, label: LabelModel) {
|
||||
public init(radioButton: RadioButtonModel, label: LabelModel, subTitle: LabelModel?) {
|
||||
self.radioButton = radioButton
|
||||
self.label = label
|
||||
self.subTitle = subTitle
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
// Form fields are items can be interacted with. They have value, and may need to be validated.
|
||||
|
||||
import VDS
|
||||
|
||||
public protocol FormFieldProtocol: FormItemProtocol {
|
||||
|
||||
@ -36,6 +36,21 @@ public extension FormFieldProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FormFieldInternalValidatableProtocol: FormFieldProtocol {
|
||||
associatedtype ValueType = AnyHashable
|
||||
var rules: [AnyRule<ValueType>]? { get set }
|
||||
var internalRules: [RuleAnyModelProtocol]? { get }
|
||||
}
|
||||
|
||||
extension FormFieldInternalValidatableProtocol {
|
||||
public var internalRules: [RuleAnyModelProtocol]? {
|
||||
guard let fieldKey else { return nil }
|
||||
return rules?.compactMap{ rule in
|
||||
return RuleVDSModel(field: fieldKey, rule: rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FormFieldValidity{
|
||||
public var fieldKey: String
|
||||
public var valid: Bool = true
|
||||
|
||||
@ -44,6 +44,35 @@ import MVMCore
|
||||
if let fieldKey = field.fieldKey {
|
||||
fields[fieldKey] = field
|
||||
}
|
||||
// add internal validators if needed
|
||||
if let field = field as? any FormFieldInternalValidatableProtocol {
|
||||
addInternalRules(field)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds additional Rules that are from another source
|
||||
private func addInternalRules(_ field: any FormFieldInternalValidatableProtocol) {
|
||||
if let internalRules = field.internalRules, !internalRules.isEmpty {
|
||||
|
||||
//find the group
|
||||
if let formGroup = formRules?.first(where: {$0.groupName == field.groupName}) {
|
||||
var appendingRules = [RulesProtocol]()
|
||||
internalRules.forEach { internalRule in
|
||||
if !formGroup.rules.contains(where: { internalRule.type == $0.type && internalRule.fields == $0.fields } ) {
|
||||
appendingRules.append(internalRule)
|
||||
}
|
||||
}
|
||||
formGroup.rules.append(contentsOf: internalRules)
|
||||
} else {
|
||||
//create the new group
|
||||
let formGroup = FormGroupRule(field.groupName, internalRules, [])
|
||||
if var formRules {
|
||||
formRules.append(formGroup)
|
||||
} else {
|
||||
formRules = [formGroup]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the form action to the validator.
|
||||
@ -72,7 +101,6 @@ import MVMCore
|
||||
if let validator = delegate?.formValidator {
|
||||
validator.delegate = delegate
|
||||
validator.insert(item)
|
||||
|
||||
// TODO: Temporary hacks, rewrite architecture to support this.
|
||||
_ = validator.validate()
|
||||
}
|
||||
|
||||
63
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
63
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// RuleVDSModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/12/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class VDSRuleBase: RuleAnyModelProtocol {
|
||||
open var ruleId: String?
|
||||
open var ruleType: String
|
||||
open var errorMessage: [String : String]?
|
||||
open var fields = [String]()
|
||||
public init(){
|
||||
ruleType = Self.identifier
|
||||
}
|
||||
public var type: String { ruleType }
|
||||
open func isValid(_ formField: any FormFieldProtocol) -> Bool {
|
||||
fatalError()
|
||||
}
|
||||
public static var identifier: String = "AnyVDSRule"
|
||||
}
|
||||
|
||||
public class RuleVDSModel<ValueType>: VDSRuleBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public var rule: AnyRule<ValueType>
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(field: String, rule: AnyRule<ValueType>) {
|
||||
self.rule = rule
|
||||
super.init()
|
||||
self.ruleType = rule.ruleType
|
||||
self.fields = [field]
|
||||
}
|
||||
|
||||
required init(from decoder: any Decoder) throws {
|
||||
fatalError("init(from:) has not been implemented")
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func isValid(_ formField: FormFieldProtocol) -> Bool {
|
||||
let value = formField.formFieldValue() as? ValueType
|
||||
let valid = rule.isValid(value: value)
|
||||
if let field = fields.first, !valid {
|
||||
errorMessage = [field: rule.errorMessage]
|
||||
} else {
|
||||
errorMessage = nil
|
||||
}
|
||||
return valid
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ open class CoreUIModelMapping: ModelMapping {
|
||||
ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self)
|
||||
|
||||
// MARK:- Entry Field
|
||||
ModelRegistry.register(handler: TextEntryField.self, for: TextEntryFieldModel.self)
|
||||
ModelRegistry.register(handler: InputEntryField.self, for: TextEntryFieldModel.self)
|
||||
ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self)
|
||||
ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self)
|
||||
ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user