Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui.git into feature/atomic-vds-checkbox

This commit is contained in:
Matt Bruce 2024-07-30 16:34:17 -05:00
commit 5a47b32789
32 changed files with 1413 additions and 1451 deletions

View File

@ -581,6 +581,8 @@
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; }; EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; }; EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.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 */; }; EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
@ -1302,6 +1306,7 @@
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */, 011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */, 0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */, FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */,
); );
name = Rules; name = Rules;
path = Rules/Rules; path = Rules/Rules;
@ -2357,6 +2362,7 @@
children = ( children = (
0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */, 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */,
0A21DB7E235DECC500C160A2 /* EntryField.swift */, 0A21DB7E235DECC500C160A2 /* EntryField.swift */,
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */,
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
@ -2845,6 +2851,7 @@
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */, D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */, EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */, 011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */,
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */, EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */, BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */, D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
@ -3148,6 +3155,7 @@
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */, 323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */, D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */, 525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */,
D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */, D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */,
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */, 0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */, D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,

View File

@ -25,6 +25,7 @@ import VDS
public var readOnly: Bool = false public var readOnly: Bool = false
public var showError: Bool? public var showError: Bool?
public var errorMessage: String? public var errorMessage: String?
public var initialErrorMessage: String?
public var fieldKey: String? public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName public var groupName: String = FormValidator.defaultGroupName
@ -89,9 +90,11 @@ import VDS
if let ruleErrorMessage = errorMessage, fieldKey != nil { if let ruleErrorMessage = errorMessage, fieldKey != nil {
self.errorMessage = ruleErrorMessage self.errorMessage = ruleErrorMessage
} else {
self.errorMessage = initialErrorMessage
} }
self.isValid = valid isValid = valid
updateUI?() updateUI?()
} }
@ -103,6 +106,7 @@ import VDS
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
initialErrorMessage = errorMessage
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false

View File

@ -345,9 +345,8 @@ import UIKit
numberOfDigits = model.digits numberOfDigits = model.digits
if let entryType = model.type { let entryType = model.type
setAsSecureTextEntry(entryType == .secure || entryType == .password) setAsSecureTextEntry(entryType == .secure || entryType == .password)
}
let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self

View File

@ -7,19 +7,66 @@
// //
import UIKit 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 // 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? { /// When selecting for first responder, allow initial selected value to appear in empty text field.
model as? ItemDropdownEntryFieldModel 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 // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -28,7 +75,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
super.init(frame: frame) super.init(frame: frame)
} }
@objc public convenience init() { @objc public convenience required init() {
self.init(frame: .zero) self.init(frame: .zero)
} }
@ -40,76 +87,134 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
@objc required public init?(coder: NSCoder) { @objc required public init?(coder: NSCoder) {
fatalError("ItemDropdownEntryField init(coder:) has not been implemented") 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 // 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. /// Sets the textField with the first value of the available picker data.
@objc private func setInitialValueFromPicker() { private func setInitialValueFromPicker() {
guard !pickerData.isEmpty else { return } guard !pickerData.isEmpty else { return }
if setInitialValueInTextField { if setInitialValueInTextField {
let pickerIndex = pickerView.selectedRow(inComponent: 0) let pickerIndex = optionsPicker.selectedRow(inComponent: 0)
itemDropdownEntryFieldModel?.selectedIndex = pickerIndex viewModel.selectedIndex = pickerIndex
observeDropdownChange?(text, pickerData[pickerIndex]) selectId = pickerIndex
text = pickerData[pickerIndex] observeDropdownChange?(selectedItem?.text, pickerData[pickerIndex])
} }
} }
@objc override func startEditing() { private func performDropdownAction() {
super.startEditing() guard let actionModel = viewModel.action,
!dropdownField.isFirstResponder
setInitialValueFromPicker() else { return }
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
} }
@objc override func endInputing() { private func updateValidation(_ isValid: Bool) {
super.endInputing() let previousValidity = self.isValid
self.isValid = isValid
guard !pickerData.isEmpty else { return } if previousValidity && !isValid {
showError = true
observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)]) } else if (!previousValidity && isValid) {
} showError = false
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)
} }
} }
//--------------------------------------------------
// 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]
}
} }

View File

@ -5,16 +5,19 @@
// Created by Kevin Christiano on 1/22/20. // Created by Kevin Christiano on 1/22/20.
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import VDS
@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel { @objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
public override class var identifier: String { "dropDown" } public override class var identifier: String { "dropDown" }
public var action: ActionModelProtocol?
public var options: [String] = [] public var options: [String] = []
public var selectedIndex: Int? public var selectedIndex: Int?
public var showInlineLabel: Bool = false
public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom
public init(with options: [String], selectedIndex: Int? = nil) { public init(with options: [String], selectedIndex: Int? = nil) {
self.options = options self.options = options
@ -42,6 +45,9 @@
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case options case options
case selectedIndex case selectedIndex
case action
case showInlineLabel
case feedbackTextPlacement
} }
//-------------------------------------------------- //--------------------------------------------------
@ -58,6 +64,9 @@
self.selectedIndex = selectedIndex self.selectedIndex = selectedIndex
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil 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 { public override func encode(to encoder: Encoder) throws {
@ -65,5 +74,8 @@
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(options, forKey: .options) try container.encode(options, forKey: .options)
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex) try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
try container.encode(showInlineLabel, forKey: .showInlineLabel)
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
try container.encodeModelIfPresent(action, forKey: .action)
} }
} }

View File

@ -315,7 +315,9 @@ import UIKit
self.showError = false self.showError = false
} }
self.isEnabled = model.enabled self.isEnabled = model.enabled
self.text = model.text if let text = model.text, !text.isEmpty {
self.text = model.text
}
}) })
} }

View File

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

View File

@ -14,7 +14,7 @@ import MVMCore
/** /**
This class provides the convenience of formatting the MDN entered/displayer for the user. 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 // MARK: - Stored Properties
//-------------------------------------------------- //--------------------------------------------------
@ -47,52 +47,17 @@ import MVMCore
get { MVMCoreUIUtility.removeMdnFormat(text) } get { MVMCoreUIUtility.removeMdnFormat(text) }
set { text = MVMCoreUIUtility.formatMdn(newValue) } 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 // MARK: - Setup
//-------------------------------------------------- //--------------------------------------------------
@objc public override func setupFieldContainerContent(_ container: UIView) { open override func setup() {
super.setupFieldContainerContent(container) super.setup()
setupTextFieldToolbar()
textField.keyboardType = .numberPad
} }
open override func setupTextFieldToolbar() { open func setupTextFieldToolbar() {
let toolbar = UIToolbar.createEmptyToolbar() let toolbar = UIToolbar.createEmptyToolbar()
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 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)) 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 // 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?) { @objc public func getContacts(_ sender: Any?) {
let picker = CNContactPickerViewController() let picker = CNContactPickerViewController()
@ -152,11 +84,12 @@ import MVMCore
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MoleculeViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------
public override func viewModelDidUpdate() {
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { viewModel.type = .phone
super.set(with: model, delegateObject, additionalData) super.viewModelDidUpdate()
if let phoneNumber = viewModel.text {
textField.keyboardType = .phonePad text = phoneNumber.formatUSNumber()
}
} }
//-------------------------------------------------- //--------------------------------------------------
@ -179,62 +112,47 @@ import MVMCore
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1) let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
unformattedMDN = String(unformedMDN[startIndex...]) unformattedMDN = String(unformedMDN[startIndex...])
} }
text = unformattedMDN text = unformattedMDN
textFieldShouldReturn(textField) textFieldShouldReturn(textField)
textFieldDidEndEditing(textField) textFieldDidEndEditing(textField)
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Implemented TextField Delegate // MARK: - Implemented TextField Delegate
//-------------------------------------------------- //--------------------------------------------------
@discardableResult @discardableResult
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { @objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool {
_ = resignFirstResponder()
textField.resignFirstResponder() let superValue = super.textFieldShouldReturn(textField)
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? superValue
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
} }
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let superValue = super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) { return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? superValue
return false
}
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
} }
@objc public func textFieldDidBeginEditing(_ textField: UITextField) { @objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
super.textFieldDidBeginEditing(textField)
textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text)
proprietorTextDelegate?.textFieldDidBeginEditing?(textField) proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
} }
@objc public func textFieldDidEndEditing(_ textField: UITextField) { @objc public override func textFieldDidEndEditing(_ textField: UITextField) {
proprietorTextDelegate?.textFieldDidEndEditing?(textField) proprietorTextDelegate?.textFieldDidEndEditing?(textField)
super.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()
}
} }
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { @objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
} }
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { @objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
} }
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool { @objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool {
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
} }
} }

View File

@ -12,4 +12,9 @@
//-------------------------------------------------- //--------------------------------------------------
public override class var identifier: String { "mdnEntryField" } 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 }
}
} }

View File

@ -11,9 +11,9 @@ import UIKit
@objc public protocol ObservingTextFieldDelegate { @objc public protocol ObservingTextFieldDelegate {
/// Called when the entered text becomes valid based on the validation block /// 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 /// 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. /// Dismisses the keyboard.
@objc optional func dismissFieldInput(_ sender: Any?) @objc optional func dismissFieldInput(_ sender: Any?)
} }
@ -317,9 +317,9 @@ import UIKit
super.shouldShowError(showError) super.shouldShowError(showError)
if showError { if showError {
observingTextFieldDelegate?.isValid?(textfield: self)
} else {
observingTextFieldDelegate?.isInvalid?(textfield: self) observingTextFieldDelegate?.isInvalid?(textfield: self)
} else {
observingTextFieldDelegate?.isValid?(textfield: self)
} }
} }

View File

@ -5,9 +5,10 @@
// Created by Kevin Christiano on 1/22/20. // Created by Kevin Christiano on 1/22/20.
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import VDS
@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatableProtocol {
@objcMembers open class TextEntryFieldModel: EntryFieldModel {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Types // MARK: - Types
//-------------------------------------------------- //--------------------------------------------------
@ -20,6 +21,39 @@
case email case email
case text case text
case phone 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 disabledTextColor: Color = Color(uiColor: .mvmCoolGray3)
public var textAlignment: NSTextAlignment = .left public var textAlignment: NSTextAlignment = .left
public var keyboardOverride: String? public var keyboardOverride: String?
public var type: EntryType? public var type: EntryType = .text
public var clearTextOnTap: Bool = false public var clearTextOnTap: Bool = false
public var displayFormat: String? public var displayFormat: String?
public var displayMask: String? public var displayMask: String?
public var enableClipboardActions: Bool = true 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 // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -114,6 +157,9 @@
case displayFormat case displayFormat
case displayMask case displayMask
case enableClipboardActions case enableClipboardActions
case tooltip
case transparentBackground
case width
} }
//-------------------------------------------------- //--------------------------------------------------
@ -128,7 +174,7 @@
displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat) displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat)
keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride) keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride)
displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask) 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) { if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) {
self.clearTextOnTap = clearTextOnTap self.clearTextOnTap = clearTextOnTap
@ -149,6 +195,10 @@
if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) { if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) {
self.enableClipboardActions = 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 { open override func encode(to encoder: Encoder) throws {
@ -164,5 +214,8 @@
try container.encode(disabledTextColor, forKey: .disabledTextColor) try container.encode(disabledTextColor, forKey: .disabledTextColor)
try container.encode(clearTextOnTap, forKey: .clearTextOnTap) try container.encode(clearTextOnTap, forKey: .clearTextOnTap)
try container.encode(enableClipboardActions, forKey: .enableClipboardActions) try container.encode(enableClipboardActions, forKey: .enableClipboardActions)
try container.encodeIfPresent(tooltip, forKey: .tooltip)
try container.encode(transparentBackground, forKey: .transparentBackground)
try container.encodeIfPresent(width, forKey: .width)
} }
} }

View File

@ -7,100 +7,60 @@
// //
import UIKit import UIKit
import VDS
open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate { //------------------------------------------------------
//--------------------------------------------------
// MARK: - Outlets
//--------------------------------------------------
open private(set) var textView: TextView = {
let textView = TextView()
textView.setContentCompressionResistancePriority(.required, for: .vertical)
return textView
}()
//--------------------------------------------------
// MARK: - Properties // 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 // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
open var shouldMaskWhileRecording: Bool {
public var textViewEntryFieldModel: TextViewEntryFieldModel? { return viewModel.shouldMaskRecordedView ?? false
model as? TextViewEntryFieldModel
} }
public override var isEnabled: Bool { /// Placeholder access for the textView.
get { super.isEnabled } open var placeholder: String? {
set (enabled) { get { viewModel?.placeholder }
super.isEnabled = enabled set {
textView.placeholder = newValue ?? ""
DispatchQueue.main.async { [weak self] in viewModel?.placeholder = newValue
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
} }
} }
/// The text of this textView. /// The text of this textView.
open override var text: String? { open override var text: String? {
get { textViewEntryFieldModel?.text } didSet {
set { viewModel?.text = text
textView.text = newValue
textViewEntryFieldModel?.text = newValue
} }
} }
/// Placeholder access for the textView. open override var errorText: String? {
public var placeholder: String? { get {
get { textViewEntryFieldModel?.placeholder } viewModel.dynamicErrorMessage ?? viewModel.errorMessage
set {
textView.placeholder = newValue ?? ""
textViewEntryFieldModel?.placeholder = newValue
textView.setPlaceholderIfAvailable()
} }
} set {}
//--------------------------------------------------
// 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
} }
//-------------------------------------------------- //--------------------------------------------------
@ -108,198 +68,178 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
//-------------------------------------------------- //--------------------------------------------------
/// The delegate and block for validation. Validates if the text that the user has entered. /// The delegate and block for validation. Validates if the text that the user has entered.
public weak var observingTextViewDelegate: ObservingTextFieldDelegate? { open 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)
}
}
}
/// If you're using a ViewController, you must set this to it /// 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 } get { textView.delegate }
set { textView.delegate = newValue } set { textView.delegate = newValue }
} }
@objc public func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) { @objc open func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
observingTextViewDelegate = delegate observingTextViewDelegate = delegate
uiTextViewDelegate = delegate uiTextViewDelegate = delegate
} }
open func setupTextViewToolbar() {
let observingDelegate = observingTextViewDelegate ?? self
textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
action: #selector(observingDelegate.dismissFieldInput))
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
open override func setup() {
@objc open override func setupFieldContainerContent(_ container: UIView) { 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) textView
leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three) .publisher(for: .editingDidBegin)
trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three) .sink { [weak self] textView in
bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three) guard let self else { return }
isEditting = true
}.store(in: &subscribers)
topConstraint?.isActive = true textView
leadingConstraint?.isActive = true .publisher(for: .editingDidEnd)
trailingConstraint?.isActive = true .sink { [weak self] textView in
bottomConstraint?.isActive = true guard let self else { return }
isEditting = false
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0) if let valid = viewModel.isValid {
accessibilityElements = [textView] 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() open func viewModelDidUpdate() {
adjustMarginConstraints(constant: Padding.Three)
heightConstraint?.constant = 0
heightConstraint?.isActive = false
}
//-------------------------------------------------- text = viewModel.text
// MARK: - Methods minHeight = viewModel.minHeight
//-------------------------------------------------- maxLength = viewModel.maxLength
/// 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()
// Don't show error till user starts typing. labelText = viewModel.title
guard text?.count ?? 0 != 0 else { helperText = viewModel.feedback
showError = false isEnabled = viewModel.enabled
return isReadOnly = viewModel.readOnly
} isRequired = viewModel.required
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
if let isValid = textViewEntryFieldModel?.isValid { width = viewModel.width
self.isValid = isValid transparentBackground = viewModel.transparentBackground
}
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
uiTextViewDelegate = delegateObject?.uiTextViewDelegate uiTextViewDelegate = delegateObject?.uiTextViewDelegate
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
if let accessibilityText = model.accessibilityText { if let accessibilityText = viewModel.accessibilityText {
accessibilityLabel = 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 switch viewModel.type {
textView.textAlignment = model.textAlignment case .secure:
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:
textView.isSecureTextEntry = true textView.isSecureTextEntry = true
textView.shouldMaskWhileRecording = true
case .numberSecure: case .numberSecure:
textView.isSecureTextEntry = true textView.isSecureTextEntry = true
textView.keyboardType = .numberPad textView.shouldMaskWhileRecording = true
case .number:
textView.keyboardType = .numberPad textView.keyboardType = .numberPad
case .email: case .email:
textView.keyboardType = .emailAddress 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. /// No point in configuring if the TextView is Read-only.
if textView.isEditable { if textView.isEditable {
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
setupTextViewToolbar()
if isSelected { if isEditting {
DispatchQueue.main.async { DispatchQueue.main.async {
_ = self.textView.becomeFirstResponder() _ = self.becomeFirstResponder()
} }
} }
} }
if model.hideBorders { viewModel.updateUI = {
adjustMarginConstraints(constant: 0) 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 previousValidity && !isValid {
showError = true
if let titleText = model.accessibilityText ?? model.title { } else if (!previousValidity && isValid) {
message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" 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
} }
set {
if let errorMessage = errorLabel.text { objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
message += ", " + errorMessage
} }
textView.accessibilityLabel = message
} }
} }

View File

@ -7,9 +7,9 @@
// //
import UIKit import UIKit
import VDS
public class TextViewEntryFieldModel: TextEntryFieldModel {
class TextViewEntryFieldModel: TextEntryFieldModel {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -17,24 +17,20 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
public override class var identifier: String { "textView" } public override class var identifier: String { "textView" }
public var accessibilityText: String? 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 editable: Bool = true
public var showsPlaceholder: Bool = false public var showsPlaceholder: Bool = false
public var minHeight: VDS.TextArea.Height = .twoX
public var maxLength: Int?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Keys // MARK: - Keys
//-------------------------------------------------- //--------------------------------------------------
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case accessibilityText case accessibilityText
case fontStyle
case height
case placeholderFontStyle
case placeholderTextColor
case editable case editable
case minHeight
case maxLength
} }
//-------------------------------------------------- //--------------------------------------------------
@ -45,34 +41,18 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
try super.init(from: decoder) try super.init(from: decoder)
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
if let placeholderFontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .placeholderFontStyle) { editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) ?? true
self.placeholderFontStyle = placeholderFontStyle minHeight = try typeContainer.decodeIfPresent(VDS.TextArea.Height.self, forKey: .minHeight) ?? .twoX
} maxLength = try typeContainer.decodeIfPresent(Int.self, forKey: .maxLength)
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
}
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
} }
public override func encode(to encoder: Encoder) throws { public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder) try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) 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(editable, forKey: .editable)
try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle) try container.encode(minHeight, forKey: .minHeight)
try container.encode(placeholderTextColor, forKey: .placeholderTextColor) try container.encodeIfPresent(maxLength, forKey: .maxLength)
} }
} }

View File

@ -5,243 +5,64 @@
// Created by Scott Pfeil on 4/9/20. // Created by Scott Pfeil on 4/9/20.
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import VDS
@objcMembers open class RadioBox: VDS.RadioBoxItem, VDSMoleculeViewProtocol, MFButtonProtocol {
open class RadioBox: Control, MFButtonProtocol { //------------------------------------------------------
//--------------------------------------------------
// MARK: - Properties // 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 func viewModelDidUpdate() {
public let subTextLabel = Label(fontStyle: .RegularMicro)
public var isOutOfStock = false
public var accentColor = UIColor.mvmRed
public let innerPadding: CGFloat = 12.0 text = viewModel.text
subText = viewModel.subText
private var borderLayer: CALayer? subTextRight = viewModel.subTextRight
private var strikeLayer: CALayer? strikethrough = viewModel.strikethrough
private var maskLayer: CALayer? isSelected = viewModel.selected
isEnabled = viewModel.enabled && !viewModel.readOnly
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() }
}
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 // MARK: - MVMCoreViewProtocol
//-------------------------------------------------- //--------------------------------------------------
open override func updateView(_ size: CGFloat) { open 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
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)
}
}
} }

View File

@ -6,6 +6,7 @@
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import MVMCore import MVMCore
import VDS
@objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol { @objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
@ -17,15 +18,17 @@ import MVMCore
public var text: String public var text: String
public var subText: String? public var subText: String?
public var subTextRight: String?
public var backgroundColor: Color? public var backgroundColor: Color?
public var accessibilityIdentifier: String? public var accessibilityIdentifier: String?
public var selectedAccentColor: Color?
public var selected: Bool = false public var selected: Bool = false
public var enabled: Bool = true public var enabled: Bool = true
public var readOnly: Bool = false public var readOnly: Bool = false
public var strikethrough: Bool = false public var strikethrough: Bool = false
public var fieldValue: String? public var fieldValue: String?
public var action: ActionModelProtocol? public var action: ActionModelProtocol?
public var inverted: Bool = false
public var surface: Surface { inverted ? .dark : .light }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Keys // MARK: - Keys
@ -36,7 +39,7 @@ import MVMCore
case moleculeName case moleculeName
case text case text
case subText case subText
case selectedAccentColor case subTextRight
case backgroundColor case backgroundColor
case accessibilityIdentifier case accessibilityIdentifier
case selected case selected
@ -45,6 +48,7 @@ import MVMCore
case fieldValue case fieldValue
case action case action
case readOnly case readOnly
case inverted
} }
//-------------------------------------------------- //--------------------------------------------------
@ -65,8 +69,7 @@ import MVMCore
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
text = try typeContainer.decode(String.self, forKey: .text) text = try typeContainer.decode(String.self, forKey: .text)
subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText) subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText)
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor) subTextRight = try typeContainer.decodeIfPresent(String.self, forKey: .subTextRight)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) { if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) {
@ -80,6 +83,10 @@ import MVMCore
strikethrough = isStrikeTrough strikethrough = isStrikeTrough
} }
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
}
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue) fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
action = try typeContainer.decodeModelIfPresent(codingKey: .action) action = try typeContainer.decodeModelIfPresent(codingKey: .action)
} }
@ -90,8 +97,7 @@ import MVMCore
try container.encode(moleculeName, forKey: .moleculeName) try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(text, forKey: .text) try container.encode(text, forKey: .text)
try container.encodeIfPresent(subText, forKey: .subText) try container.encodeIfPresent(subText, forKey: .subText)
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor) try container.encodeIfPresent(subTextRight, forKey: .subTextRight)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
try container.encode(selected, forKey: .selected) try container.encode(selected, forKey: .selected)
try container.encode(enabled, forKey: .enabled) try container.encode(enabled, forKey: .enabled)
@ -99,5 +105,6 @@ import MVMCore
try container.encode(strikethrough, forKey: .strikethrough) try container.encode(strikethrough, forKey: .strikethrough)
try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(action, forKey: .action)
try container.encode(inverted, forKey: .inverted)
} }
} }

View File

@ -7,172 +7,69 @@
// //
import Foundation import Foundation
import VDS
public protocol RadioBoxSelectionDelegate: AnyObject { public protocol RadioBoxSelectionDelegate: AnyObject {
func selected(radioBox: RadioBoxModel) func selected(radioBox: RadioBoxModel)
} }
open class RadioBoxes: View { open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol {
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?
//------------------------------------------------------
// 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. /// The models for the molecules.
public var boxes: [RadioBoxModel]? public var boxes: [RadioBoxModel]?
public weak var radioDelegate: RadioBoxSelectionDelegate?
private var size: CGFloat? // TODO: this matches the current accessibility however not what was passed by Barbara's team.
// open override var items: [RadioBoxItem] {
open override func layoutSubviews() { // didSet {
super.layoutSubviews() // let total = items.count
// Accounts for any collection size changes // for (index, radioBoxItem) in items.enumerated() {
DispatchQueue.main.async { // radioBoxItem.selectorView.bridge_accessibilityValueBlock = {
self.collectionView.collectionViewLayout.invalidateLayout() // 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 // MARK: - MoleculeViewProtocol
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { public func viewModelDidUpdate() {
super.set(with: model, delegateObject, additionalData) boxes = viewModel.boxes
self.delegateObject = delegateObject 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 // 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] {
registerCells() boxes?.forEach {$0.selected = false }
setHeight() selectedBox.selected = true
collectionView.reloadData() _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
} radioDelegate?.selected(radioBox: selectedBox)
@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
} }
// 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()
} }
} }

View File

@ -6,32 +6,36 @@
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import MVMCore import MVMCore
import VDS
@objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol { @objcMembers public class RadioBoxesModel: FormFieldModel {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
public static var identifier: String = "radioBoxes" public override static var identifier: String { "radioBoxes" }
public var id: String = UUID().uuidString
public var boxes: [RadioBoxModel] public var boxes: [RadioBoxModel]
public var backgroundColor: Color?
public var accessibilityIdentifier: String? public var selectorModels: [VDS.RadioBoxGroup.RadioBoxItemModel] {
public var selectedAccentColor: Color? boxes.compactMap({ item in
public var boxesColor: Color? var radioBox = RadioBoxGroup.RadioBoxItemModel()
public var fieldKey: String? radioBox.text = item.text
public var groupName: String = FormValidator.defaultGroupName radioBox.subText = item.subText
public var baseValue: AnyHashable? radioBox.subTextRight = item.subTextRight
public var enabled: Bool = true radioBox.surface = surface
public var readOnly: Bool = false radioBox.selected = item.selected
radioBox.strikethrough = item.strikethrough
radioBox.disabled = !(item.enabled && !item.readOnly)
return radioBox
})
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Form Validation // MARK: - Form Validation
//-------------------------------------------------- //--------------------------------------------------
/// Returns the fieldValue of the selected box, otherwise the text of the selected box. /// 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 } guard enabled else { return nil }
let selectedBox = boxes.first { (box) -> Bool in let selectedBox = boxes.first { (box) -> Bool in
return box.selected return box.selected
@ -42,7 +46,7 @@ import MVMCore
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Server Value // MARK: - Server Value
//-------------------------------------------------- //--------------------------------------------------
open func formFieldServerValue() -> AnyHashable? { open override func formFieldServerValue() -> AnyHashable? {
return formFieldValue() return formFieldValue()
} }
@ -51,17 +55,7 @@ import MVMCore
//-------------------------------------------------- //--------------------------------------------------
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id
case moleculeName
case enabled
case readOnly
case selectedAccentColor
case backgroundColor
case accessibilityIdentifier
case boxesColor
case boxes case boxes
case fieldKey
case groupName
} }
//-------------------------------------------------- //--------------------------------------------------
@ -69,7 +63,8 @@ import MVMCore
//-------------------------------------------------- //--------------------------------------------------
public init(with boxes: [RadioBoxModel]){ 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 { required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) 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) boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes)
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) try super.init(from: decoder)
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()
} }
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) 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.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)
} }
} }

View File

@ -7,37 +7,35 @@
// //
import UIKit import UIKit
import VDSCoreTokens import VDS
@objcMembers open class RadioButton: Control, MFButtonProtocol { @objcMembers open class RadioButton: VDS.RadioButton, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol, MVMCoreUIViewConstrainingProtocol {
//-------------------------------------------------- //------------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //------------------------------------------------------
open var viewModel: RadioButtonModel!
public var diameter: CGFloat = 20 { open var delegateObject: MVMCoreUIDelegateObject?
didSet { widthConstraint?.constant = diameter } 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 { didSet {
radioModel?.state = isSelected viewModel.state = isSelected
updateAccessibilityLabel() if oldValue != isSelected {
sendActions(for: .valueChanged)
}
} }
} }
public var enabledColor: UIColor {
return radioModel?.inverted ?? false ? VDSColor.elementsPrimaryOndark : VDSColor.elementsPrimaryOnlight lazy public var radioGroupName: String? = { viewModel.fieldKey }()
}
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 radioButtonSelectionHelper: RadioButtonSelectionHelper? = { lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
@ -48,132 +46,108 @@ import VDSCoreTokens
return radioButtonModel return radioButtonModel
}() }()
public override var isEnabled: Bool { //--------------------------------------------------
didSet { // MARK: - Initializers
isUserInteractionEnabled = isEnabled //--------------------------------------------------
setNeedsDisplay()
} override public init(frame: CGRect) {
super.init(frame: frame)
} }
//-------------------------------------------------- /// There is currently no intention on using xib files.
// MARK: - Constraints required public init?(coder aDecoder: NSCoder) {
//-------------------------------------------------- super.init(coder: aDecoder)
fatalError("xib file is not implemented for Checkbox.")
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()
}
} }
public convenience required init() {
self.init(frame:.zero)
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Validation // MARK: - Validation
//-------------------------------------------------- //--------------------------------------------------
/// The action performed when tapped. public func isValidField() -> Bool { isSelected }
func tapAction() {
if !isEnabled { public func formFieldName() -> String? {
return 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 let wasPreviouslySelected = isSelected
if let radioButtonModel = radioButtonSelectionHelper { if let radioButtonSelectionHelper {
radioButtonModel.selected(self) radioButtonSelectionHelper.selected(self)
} else { } else {
isSelected = !isSelected isSelected = !isSelected
} }
if let radioModel = radioModel, let actionModel = radioModel.action, isSelected, !wasPreviouslySelected { if let actionModel = viewModel.action, isSelected, !wasPreviouslySelected {
Task(priority: .userInitiated) { 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) _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
setNeedsDisplay() setNeedsUpdate()
}
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
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Methods // MARK: - Actions
//-------------------------------------------------- //--------------------------------------------------
/// Adjust accessibility label based on state of RadioButton. /// This will toggle the state of the Checkbox and execute the actionBlock if provided.
func updateAccessibilityLabel() { public func tapAction() {
if let message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button"), toggle()
let selectedState = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "radio_selected_state" : "radio_not_selected_state") {
accessibilityLabel = message + selectedState
}
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MVMViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------
open override func setupView() { open func needsToBeConstrained() -> Bool { true }
super.setupView()
backgroundColor = .clear public func horizontalAlignment() -> UIStackView.Alignment { .leading }
clipsToBounds = true
widthConstraint = widthAnchor.constraint(equalToConstant: 20) public func updateView(_ size: CGFloat) {}
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 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
}
} }

View File

@ -7,48 +7,29 @@
// //
import MVMCore import MVMCore
import VDS
open class RadioButtonModel: FormFieldModel {
open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
public static var identifier: String = "radioButton" public static override var identifier: String { "radioButton" }
public var id: String = UUID().uuidString
public var backgroundColor: Color?
public var accessibilityIdentifier: String?
public var state: Bool = false 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. /// The specific value to send to server. TODO: update this to be more generic.
public var fieldValue: String? public var fieldValue: String?
public var baseValue: AnyHashable?
public var groupName: String = FormValidator.defaultGroupName
public var fieldKey: String?
public var action: ActionModelProtocol? public var action: ActionModelProtocol?
public var inverted: Bool = false
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Keys // MARK: - Keys
//-------------------------------------------------- //--------------------------------------------------
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id
case moleculeName
case backgroundColor
case accessibilityIdentifier
case state case state
case enabled
case fieldValue case fieldValue
case fieldKey
case groupName
case action case action
case readOnly
case inverted
} }
//-------------------------------------------------- //--------------------------------------------------
@ -56,69 +37,50 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
//-------------------------------------------------- //--------------------------------------------------
public init(_ state: Bool) { public init(_ state: Bool) {
super.init()
self.state = state self.state = state
baseValue = state self.baseValue = state
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Validation // MARK: - Validation
//-------------------------------------------------- //--------------------------------------------------
public func formFieldValue() -> AnyHashable? { public override func formFieldValue() -> AnyHashable? {
guard enabled else { return nil } guard enabled else { return nil }
return fieldValue return fieldValue
} }
//-------------------------------------------------- open override func setValidity(_ valid: Bool, errorMessage: String?) {
// MARK: - Server Value if let ruleErrorMessage = errorMessage, fieldKey != nil {
//-------------------------------------------------- self.errorMessage = ruleErrorMessage
open func formFieldServerValue() -> AnyHashable? { }
return formFieldValue() isValid = valid
updateUI?()
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Codec // MARK: - Codec
//-------------------------------------------------- //--------------------------------------------------
required public init(from decoder: Decoder) throws { required public init(from decoder: Decoder) throws {
try super.init(from: decoder)
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) 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) { if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) {
self.state = 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 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) fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
action = try typeContainer.decodeModelIfPresent(codingKey: .action) 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) 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(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.encodeIfPresent(fieldValue, forKey: .fieldValue)
try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(action, forKey: .action)
try container.encodeIfPresent(inverted, forKey: .inverted)
} }
} }

View File

@ -6,6 +6,10 @@
// Copyright © 2019 Verizon Wireless. All rights reserved. // 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 { @objcMembers public class RadioButtonSelectionHelper: FormFieldProtocol {
//-------------------------------------------------- //--------------------------------------------------
@ -14,7 +18,7 @@
public var fieldKey: String? public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName public var groupName: String = FormValidator.defaultGroupName
private var selectedRadioButton: RadioButton? private var selectedRadioButton: RadioButtonSelectionHelperProtocol?
private var selectedRadioButtonModel: RadioButtonModel? private var selectedRadioButtonModel: RadioButtonModel?
public var baseValue: AnyHashable? public var baseValue: AnyHashable?
public var enabled: Bool = true public var enabled: Bool = true
@ -24,7 +28,7 @@
// MARK: - Initializer // MARK: - Initializer
//-------------------------------------------------- //--------------------------------------------------
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) { public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol) {
self.fieldKey = radioButtonModel.fieldKey self.fieldKey = radioButtonModel.fieldKey
self.groupName = radioButtonModel.groupName self.groupName = radioButtonModel.groupName
@ -49,7 +53,7 @@
// MARK: - Functions // 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, guard let groupName = radioButtonModel.fieldKey,
let formValidator = delegateObject?.formHolderDelegate?.formValidator let formValidator = delegateObject?.formHolderDelegate?.formValidator
@ -61,10 +65,10 @@
FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate) FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate)
} }
public func selected(_ radioButton: RadioButton) { public func selected(_ radioButton: RadioButtonSelectionHelperProtocol) {
// Checks because the view could be reused // Checks because the view could be reused
if selectedRadioButton?.radioModel === selectedRadioButtonModel { if selectedRadioButton?.radioButtonModel === selectedRadioButtonModel {
selectedRadioButton?.isSelected = false selectedRadioButton?.isSelected = false
} else { } else {
selectedRadioButtonModel?.state = false selectedRadioButtonModel?.state = false
@ -72,7 +76,7 @@
selectedRadioButton = radioButton selectedRadioButton = radioButton
selectedRadioButton?.isSelected = true selectedRadioButton?.isSelected = true
selectedRadioButtonModel = selectedRadioButton?.radioModel selectedRadioButtonModel = selectedRadioButton?.radioButtonModel
} }
} }

View File

@ -8,6 +8,7 @@
import MVMCore import MVMCore
import UIKit import UIKit
import VDS
public typealias ActionBlockConfirmation = () -> (Bool) public typealias ActionBlockConfirmation = () -> (Bool)
@ -19,137 +20,40 @@ public typealias ActionBlockConfirmation = () -> (Bool)
Container: The background of the toggle control. Container: The background of the toggle control.
Knob: The circular indicator that slides on the container. 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 // 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 didToggleAction: ActionBlock? {
public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack) didSet {
if let didToggleAction {
/// Holds the on and off colors for the knob. onChange = { _ in
public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite) didToggleAction()
}
/// Holds the on and off colors for the disabled state.. } else {
public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite) onChange = nil
}
/// Set this flag to false if you do not want to animate state changes. }
public var isAnimated = true }
public var didToggleAction: ActionBlock?
/// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute. /// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute.
public var shouldToggleAction: ActionBlockConfirmation? = { public var shouldToggleAction: ActionBlockConfirmation? = {
return { true } 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 // 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. /// Simple means to prevent user interaction with the toggle.
public var isLocked: Bool = false { public var isLocked: Bool = false {
didSet { isUserInteractionEnabled = !isLocked } 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 // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -158,7 +62,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
super.init(frame: frame) super.init(frame: frame)
} }
public convenience override init() { public convenience required init() {
self.init(frame: .zero) self.init(frame: .zero)
} }
@ -171,7 +75,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
/// - parameter didToggleAction: A closure which is executed after the toggle changes states. /// - parameter didToggleAction: A closure which is executed after the toggle changes states.
public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) { public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) {
self.init(frame: .zero) self.init(frame: .zero)
changeStateNoAnimation(isOn) self.isOn = isOn
self.didToggleAction = didToggleAction self.didToggleAction = didToggleAction
} }
@ -191,223 +95,75 @@ public typealias ActionBlockConfirmation = () -> (Bool)
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Lifecycle // 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() { public override func reset() {
super.reset() super.reset()
backgroundColor = containerTintColor.off
knobView.backgroundColor = knobTintColor.off
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
isAnimated = true
didToggleAction = nil didToggleAction = nil
shouldToggleAction = { return true } shouldToggleAction = { return true }
} }
class func getContainerWidth() -> CGFloat { public func viewModelDidUpdate() {
let containerWidth = Self.containerSize.width FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
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() {
if let result = shouldToggleAction?(), result { isOn = viewModel.selected
isOn.toggle() surface = viewModel.surface
didToggleAction?() isAnimated = viewModel.animated
isEnabled = viewModel.enabled && !viewModel.readOnly
showText = viewModel.showText
if let onText = viewModel.onText {
self.onText = onText
} }
} if let offText = viewModel.offText {
self.offText = offText
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()
} }
} textSize = viewModel.textSize
textWeight = viewModel.textWeight
// MARK:- MoleculeViewProtocol textPosition = viewModel.textPosition
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
guard let model = model as? ToggleModel else { return } if let accessibileString = viewModel.accessibilityText {
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 {
accessibilityLabel = accessibileString accessibilityLabel = accessibileString
} }
if model.action != nil || model.alternateAction != nil { if viewModel.action != nil || viewModel.alternateAction != nil {
didToggleAction = { [weak self] in didToggleAction = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if self.isOn { if self.isOn {
if let action = model.action { if let action = viewModel.action {
MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
} }
} else { } 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) 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? { open override func toggle() {
Self.getContainerHeight() 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
} }
} }

View File

@ -5,7 +5,7 @@
// Created by Scott Pfeil on 1/14/20. // Created by Scott Pfeil on 1/14/20.
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
import VDS
public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
//-------------------------------------------------- //--------------------------------------------------
@ -24,10 +24,15 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
public var action: ActionModelProtocol? public var action: ActionModelProtocol?
public var alternateAction: ActionModelProtocol? public var alternateAction: ActionModelProtocol?
public var accessibilityText: String? public var accessibilityText: String?
public var onTintColor: Color = Color(uiColor: .mvmGreen)
public var offTintColor: Color = Color(uiColor: .mvmBlack) public var surface: Surface { inverted ? .dark : .light }
public var onKnobTintColor: Color = Color(uiColor: .mvmWhite) public var inverted: Bool = false
public var offKnobTintColor: Color = Color(uiColor: .mvmWhite) 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 fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName public var groupName: String = FormValidator.defaultGroupName
@ -49,10 +54,15 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
case accessibilityIdentifier case accessibilityIdentifier
case alternateAction case alternateAction
case accessibilityText case accessibilityText
case onTintColor
case offTintColor case inverted
case onKnobTintColor case showText
case offKnobTintColor case onText
case offText
case textSize
case textWeight
case textPosition
case fieldKey case fieldKey
case groupName case groupName
} }
@ -102,25 +112,8 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
action = try typeContainer.decodeModelIfPresent(codingKey: .action) action = try typeContainer.decodeModelIfPresent(codingKey: .action)
alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) 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) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
baseValue = selected baseValue = selected
@ -130,6 +123,14 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
} }
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false 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 { 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(selected, forKey: .state)
try container.encode(animated, forKey: .animated) try container.encode(animated, forKey: .animated)
try container.encode(enabled, forKey: .enabled) 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(accessibilityText, forKey: .accessibilityText)
try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
try container.encodeIfPresent(groupName, forKey: .groupName) try container.encodeIfPresent(groupName, forKey: .groupName)
try container.encode(readOnly, forKey: .readOnly) 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)
} }
} }

View File

@ -34,11 +34,7 @@ public typealias ActionBlock = () -> ()
/// A specific text index to use as a unique marker. /// A specific text index to use as a unique marker.
public var hero: Int? public var hero: Int?
public var getRange: NSRange {
NSRange(location: 0, length: text?.count ?? 0)
}
public var shouldMaskWhileRecording: Bool = false public var shouldMaskWhileRecording: Bool = false
public var hasText: Bool { public var hasText: Bool {
@ -378,19 +374,24 @@ extension Label {
// MARK: - Atomization // MARK: - Atomization
extension Label { extension Label {
public func needsToBeConstrained() -> Bool { true } public func needsToBeConstrained() -> Bool { true }
public func horizontalAlignment() -> UIStackView.Alignment { .leading } public func horizontalAlignment() -> UIStackView.Alignment { .leading }
public func copyBackgroundColor() -> Bool { true } public func copyBackgroundColor() -> Bool { true }
} }
// MARK: - Multi-Link Functionality // 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. /// 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 } guard let text, text.isValid(range: range) else { return }
var textLink = ActionLabelAttribute(location: range.location, length: range.length) var textLink = ActionLabelAttribute(location: range.location, length: range.length)
@ -417,8 +418,16 @@ extension Label {
return { [weak self] in return { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { if let button = self as? MFButtonProtocol {
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) 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)
} }
} }
} }

View File

@ -81,9 +81,7 @@
func updateAccessibilityLabel() { func updateAccessibilityLabel() {
var message = "" var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel { if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", " message += radioButtonLabel + ", "
} }

View File

@ -98,9 +98,7 @@ import UIKit
func updateAccessibilityLabel() { func updateAccessibilityLabel() {
var message = "" var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel { if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", " message += radioButtonLabel + ", "
} }

View File

@ -85,7 +85,6 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell {
var message = "" var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel { if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", " message += radioButtonLabel + ", "
} }

View File

@ -7,54 +7,121 @@
// //
import UIKit import UIKit
import VDS
@objcMembers public class RadioButtonLabel: VDS.RadioButtonItem, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol {
@objcMembers public class RadioButtonLabel: View { //------------------------------------------------------
// MARK: - Properties
public let radioButton = RadioButton() //------------------------------------------------------
var delegateObject: MVMCoreUIDelegateObject? open var viewModel: RadioButtonLabelModel!
let label = Label() open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
public override func updateView(_ size: CGFloat) {
super.updateView(size)
radioButton.updateView(size)
label.updateView(size)
}
open override func setupView() { open var radioButtonModel: RadioButtonModel {
super.setupView() viewModel.radioButton
}
addSubview(radioButton) // Form Validation
radioButton.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: 0).isActive = true var fieldKey: String?
radioButton.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: PaddingOne).isActive = true var fieldValue: JSONValue?
layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: radioButton.bottomAnchor, constant: PaddingOne).isActive = true var groupName: String?
radioButton.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor).isActive = true
if let rightView = createRightView() { open override var isSelected: Bool {
addSubview(rightView) didSet {
rightView.leftAnchor.constraint(equalTo: radioButton.rightAnchor, constant: Padding.Component.gutterForApplicationWidth).isActive = true radioButtonModel.state = isSelected
rightView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: 0).isActive = true if oldValue != isSelected {
sendActions(for: .valueChanged)
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
} }
} }
func createRightView() -> Container? { lazy public var radioGroupName: String? = { viewModel.radioButton.fieldKey }()
let rightView = Container(andContain: label)
return rightView 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 } // MARK: - Atomic
//--------------------------------------------------
radioButton.set(with: radioButtonLabelModel.radioButton, delegateObject, additionalData) public func viewModelDidUpdate() {
label.set(with: radioButtonLabelModel.label, delegateObject, additionalData) 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
})
}
}
} }

View File

@ -8,8 +8,9 @@
import Foundation import Foundation
import MVMCore import MVMCore
import VDS
@objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol { @objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -21,14 +22,23 @@ import MVMCore
public var moleculeName: String = RadioButtonLabelModel.identifier public var moleculeName: String = RadioButtonLabelModel.identifier
public var radioButton: RadioButtonModel public var radioButton: RadioButtonModel
public var label: LabelModel 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 // MARK: - Initializer
//-------------------------------------------------- //--------------------------------------------------
public init(radioButton: RadioButtonModel, label: LabelModel) { public init(radioButton: RadioButtonModel, label: LabelModel, subTitle: LabelModel?) {
self.radioButton = radioButton self.radioButton = radioButton
self.label = label self.label = label
self.subTitle = subTitle
} }
} }

View File

@ -6,7 +6,7 @@
// Copyright © 2020 Verizon Wireless. All rights reserved. // Copyright © 2020 Verizon Wireless. All rights reserved.
// //
// Form fields are items can be interacted with. They have value, and may need to be validated. // Form fields are items can be interacted with. They have value, and may need to be validated.
import VDS
public protocol FormFieldProtocol: FormItemProtocol { 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 class FormFieldValidity{
public var fieldKey: String public var fieldKey: String
public var valid: Bool = true public var valid: Bool = true

View File

@ -44,6 +44,35 @@ import MVMCore
if let fieldKey = field.fieldKey { if let fieldKey = field.fieldKey {
fields[fieldKey] = field 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. /// Adds the form action to the validator.
@ -72,7 +101,6 @@ import MVMCore
if let validator = delegate?.formValidator { if let validator = delegate?.formValidator {
validator.delegate = delegate validator.delegate = delegate
validator.insert(item) validator.insert(item)
// TODO: Temporary hacks, rewrite architecture to support this. // TODO: Temporary hacks, rewrite architecture to support this.
_ = validator.validate() _ = validator.validate()
} }

View 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
}
}

View File

@ -41,7 +41,7 @@ open class CoreUIModelMapping: ModelMapping {
ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self) ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self)
// MARK:- Entry Field // 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: MdnEntryField.self, for: MdnEntryFieldModel.self)
ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self) ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self)
ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self) ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self)