Compare commits

..

No commits in common. "25730bbda30ce9944c3a49319caf70da21a8c664" and "cadded321776a8b1265558cbbef8797e13cee132" have entirely different histories.

8 changed files with 423 additions and 429 deletions

View File

@ -578,7 +578,6 @@
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 */; };
EA1B02DA2C407BD600F0758B /* LegacyTextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02D92C407BD600F0758B /* LegacyTextEntryField.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 */; };
@ -1200,7 +1199,6 @@
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>"; };
EA1B02D92C407BD600F0758B /* LegacyTextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTextEntryField.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>"; };
@ -2352,7 +2350,6 @@
0A21DB7E235DECC500C160A2 /* EntryField.swift */, 0A21DB7E235DECC500C160A2 /* EntryField.swift */,
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
EA1B02D92C407BD600F0758B /* LegacyTextEntryField.swift */,
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */, 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */,
0A8321AE2355FE9500CB7F00 /* DigitBox.swift */, 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */,
@ -2848,7 +2845,6 @@
942C378C2412F4FA0066E45E /* ModalMoleculeListTemplate.swift in Sources */, 942C378C2412F4FA0066E45E /* ModalMoleculeListTemplate.swift in Sources */,
BB47A588241615FA002BB23C /* ListOneColumnFullWidthTextDividerSubsection.swift in Sources */, BB47A588241615FA002BB23C /* ListOneColumnFullWidthTextDividerSubsection.swift in Sources */,
012A88C8238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift in Sources */, 012A88C8238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift in Sources */,
EA1B02DA2C407BD600F0758B /* LegacyTextEntryField.swift in Sources */,
8D8067D12444472F00203BE8 /* ListRightVariablePriceChangeAllTextAndLinksModel.swift in Sources */, 8D8067D12444472F00203BE8 /* ListRightVariablePriceChangeAllTextAndLinksModel.swift in Sources */,
0A7EF86123D8AC2500B2AAD1 /* DigitEntryFieldModel.swift in Sources */, 0A7EF86123D8AC2500B2AAD1 /* DigitEntryFieldModel.swift in Sources */,
D224798C231450C8003FCCF9 /* HeadlineBodyToggle.swift in Sources */, D224798C231450C8003FCCF9 /* HeadlineBodyToggle.swift in Sources */,

View File

@ -11,7 +11,7 @@ import UIKit
/** /**
* Subclass of TextEntryField as it is to use similar logic as a singular textField but appear separate.. * Subclass of TextEntryField as it is to use similar logic as a singular textField but appear separate..
*/ */
@objcMembers open class DigitEntryField: LegacyTextEntryField, DigitBoxProtocol { @objcMembers open class DigitEntryField: TextEntryField, DigitBoxProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Stored Properties // MARK: - Stored Properties
//-------------------------------------------------- //--------------------------------------------------
@ -345,9 +345,9 @@ import UIKit
numberOfDigits = model.digits numberOfDigits = model.digits
let entryType = model.type if 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

@ -12,7 +12,7 @@ import MVMCore
This class is intended to be subclassed. This class is intended to be subclassed.
See ItemDropdownEntryField and DateDropdownEntryField. See ItemDropdownEntryField and DateDropdownEntryField.
*/ */
@objcMembers open class BaseDropdownEntryField: LegacyTextEntryField { @objcMembers open class BaseDropdownEntryField: TextEntryField {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Outlets // MARK: - Outlets
//-------------------------------------------------- //--------------------------------------------------
@ -48,9 +48,6 @@ import MVMCore
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@objc public convenience init() {
self.init(frame: .zero)
}
@objc public override init(frame: CGRect) { @objc public override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)

View File

@ -48,8 +48,50 @@ import MVMCore
set { text = MVMCoreUIUtility.formatMdn(newValue) } set { text = MVMCoreUIUtility.formatMdn(newValue) }
} }
open override func setup() { /// Toggles selected or original (unselected) UI.
super.setup() public override var isSelected: Bool {
get { return entryFieldContainer.isSelected }
set (selected) {
if selected && showError {
showError = false
}
super.isSelected = selected
}
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@objc public override init(frame: CGRect) {
super.init(frame: .zero)
}
@objc public convenience init() {
self.init(frame: .zero)
}
@objc required public init?(coder: NSCoder) {
super.init(coder: coder)
fatalError("MdnEntryField xib not supported.")
}
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.init(model: model, delegateObject, additionalData)
}
//--------------------------------------------------
// MARK: - Setup
//--------------------------------------------------
@objc public override func setupFieldContainerContent(_ container: UIView) {
super.setupFieldContainerContent(container)
textField.keyboardType = .numberPad
}
open override func 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)
@ -57,17 +99,12 @@ import MVMCore
let dismissButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissFieldInput)) let dismissButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissFieldInput))
toolbar.items = [contacts, space, dismissButton] toolbar.items = [contacts, space, dismissButton]
textField.inputAccessoryView = toolbar textField.inputAccessoryView = toolbar
} }
open override var viewModel: TextEntryFieldModel! {
didSet {
viewModel.type = .phone
}
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Methods // MARK: - Methods
//-------------------------------------------------- //--------------------------------------------------
@objc public func hasValidMDN() -> Bool { @objc public func hasValidMDN() -> Bool {
guard let MDN = mdn, !MDN.isEmpty else { return false } guard let MDN = mdn, !MDN.isEmpty else { return false }
@ -92,7 +129,7 @@ import MVMCore
showError = false showError = false
} else { } else {
viewModel?.errorMessage = viewModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message") entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message")
showError = true showError = true
UIAccessibility.post(notification: .layoutChanged, argument: textField) UIAccessibility.post(notification: .layoutChanged, argument: textField)
} }
@ -107,8 +144,8 @@ import MVMCore
picker.displayedPropertyKeys = ["phoneNumbers"] picker.displayedPropertyKeys = ["phoneNumbers"]
picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0") picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0")
picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'") picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'")
if let topViewController = UIApplication.topViewController() { Task(priority: .userInitiated) {
topViewController.present(picker, animated: true) await NavigationHandler.shared().present(viewController: picker, animated: true)
} }
} }
@ -116,6 +153,11 @@ import MVMCore
// MARK: - MoleculeViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
textField.keyboardType = .phonePad
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Contact Picker Delegate // MARK: - Contact Picker Delegate
@ -126,11 +168,21 @@ import MVMCore
if let phoneNumber = contactProperty.value as? CNPhoneNumber { if let phoneNumber = contactProperty.value as? CNPhoneNumber {
let MDN = phoneNumber.stringValue let MDN = phoneNumber.stringValue
let unformattedMDN = MVMCoreUIUtility.removeMdnFormat(MDN) var unformattedMDN = MVMCoreUIUtility.removeMdnFormat(MDN)
// Sometimes user add extra 1 in front of mdn in their address book
if isNationalMDN,
let unformedMDN = unformattedMDN,
unformedMDN.count == 11,
unformedMDN[(unformedMDN.index(unformedMDN.startIndex, offsetBy: 0))] == "1" {
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
unformattedMDN = String(unformedMDN[startIndex...])
}
text = unformattedMDN text = unformattedMDN
textFieldShouldReturn(textField) textFieldShouldReturn(textField)
textFieldDidEndEditing(textField) textFieldDidEndEditing(textField)
_ = resignFirstResponder()
} }
} }
@ -138,49 +190,51 @@ import MVMCore
// MARK: - Implemented TextField Delegate // MARK: - Implemented TextField Delegate
//-------------------------------------------------- //--------------------------------------------------
// @discardableResult @discardableResult
// @objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool { @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
//
// textField.resignFirstResponder() textField.resignFirstResponder()
//
// return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
// } }
//
// @objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//
// if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) { if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) {
// return false return false
// } }
//
// return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
// } }
//
// @objc public override func textFieldDidBeginEditing(_ textField: UITextField) { @objc public func textFieldDidBeginEditing(_ textField: UITextField) {
//
// textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text) textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text)
// proprietorTextDelegate?.textFieldDidBeginEditing?(textField) proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
// } }
//
// @objc public override func textFieldDidEndEditing(_ textField: UITextField) { @objc public func textFieldDidEndEditing(_ textField: UITextField) {
//
// proprietorTextDelegate?.textFieldDidEndEditing?(textField) proprietorTextDelegate?.textFieldDidEndEditing?(textField)
//
// if validateMDNTextField() { if validateMDNTextField() {
// if isNationalMDN { if isNationalMDN {
// textField.text = MVMCoreUIUtility.formatMdn(textField.text) textField.text = MVMCoreUIUtility.formatMdn(textField.text)
// } }
// } // Validate the base input field along with triggering form field validation rules.
// } validateText()
// }
// @objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { }
// proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
// } @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
// proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
// @objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { }
// proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
// } @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
// proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
// @objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool { }
// proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
// } @objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
}
} }

View File

@ -7,45 +7,43 @@
// //
import UIKit import UIKit
import VDS
@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: EntryField?) @objc optional func isValid(textfield: TextEntryField?)
/// 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: EntryField?) @objc optional func isInvalid(textfield: TextEntryField?)
/// Dismisses the keyboard. /// Dismisses the keyboard.
@objc optional func dismissFieldInput(_ sender: Any?) @objc optional func dismissFieldInput(_ sender: Any?)
} }
@objcMembers open class TextEntryField: VDS.InputField, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol { @objcMembers open class TextEntryField: EntryField, UITextFieldDelegate, ObservingTextFieldDelegate {
//------------------------------------------------------
// 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 // MARK: - Outlets
//-------------------------------------------------- //--------------------------------------------------
public var isValid: Bool = true
/// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well. open private(set) var textField: TextField = {
private weak var proprietorTextDelegate: UITextFieldDelegate? let textField = TextField()
textField.isAccessibilityElement = true
textField.setContentCompressionResistancePriority(.required, for: .vertical)
textField.font = Styler.Font.RegularBodyLarge.getFont()
textField.textColor = .mvmBlack
textField.smartQuotesType = .no
textField.smartDashesType = .no
textField.smartInsertDeleteType = .no
return textField
}()
private var isEditting: Bool = false { public lazy var errorImage: UIImageView = {
didSet { let image = MVMCoreUIUtility.imageNamed("alert_standard")
viewModel.selected = isEditting let imageView = UIImageView(image: image)
} imageView.translatesAutoresizingMaskIntoConstraints = false
} imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
return imageView
}()
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Stored Properties // MARK: - Stored Properties
@ -54,19 +52,52 @@ import VDS
private var observingForChange: Bool = false private var observingForChange: Bool = false
/// Validate when user resigns editing. Default: true /// Validate when user resigns editing. Default: true
open var validateWhenDoneEditing: Bool = true public var validateWhenDoneEditing: Bool = true
public var textEntryFieldModel: TextEntryFieldModel? { model as? TextEntryFieldModel }
open var shouldMaskWhileRecording: Bool {
return viewModel.shouldMaskRecordedView ?? false
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Computed Properties // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
public override var isEnabled: Bool {
get { super.isEnabled }
set (enabled) {
super.isEnabled = enabled
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.textField.isEnabled = enabled
self.textField.textColor = enabled ? self.textEntryFieldModel?.enabledTextColor.uiColor : self.textEntryFieldModel?.disabledTextColor.uiColor
}
}
}
public override var showError: Bool {
get { super.showError }
set (error) {
if error {
textField.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textfield_error_message") ?? "", textField.text ?? "", entryFieldModel?.errorMessage ?? "")
} else {
textField.accessibilityValue = nil
}
if !textField.isSecureTextEntry {
showErrorView(error)
}
super.showError = error
}
}
/// The text of this TextField. /// The text of this TextField.
open override var text: String? { open override var text: String? {
didSet { get { textField.text }
viewModel?.text = text set {
textEntryFieldModel?.text = newValue
textField.text = newValue
} }
} }
@ -79,77 +110,173 @@ import VDS
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Delegate Properties // MARK: - Delegate Properties
//-------------------------------------------------- //--------------------------------------------------
/// 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 observingTextFieldDelegate: ObservingTextFieldDelegate? public weak var observingTextFieldDelegate: ObservingTextFieldDelegate? {
didSet {
if observingTextFieldDelegate != nil && !observingForChange {
observingForChange = true
NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextField.textDidChangeNotification, object: textField)
NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextField.textDidEndEditingNotification, object: textField)
NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextField.textDidBeginEditingNotification, object: textField)
} else if observingTextFieldDelegate == nil && observingForChange {
observingForChange = false
NotificationCenter.default.removeObserver(self, name: UITextField.textDidChangeNotification, object: textField)
NotificationCenter.default.removeObserver(self, name: UITextField.textDidEndEditingNotification, object: textField)
NotificationCenter.default.removeObserver(self, name: UITextField.textDidBeginEditingNotification, object: textField)
}
}
}
/// If you're using a ViewController, you must set this to it /// If you're using a ViewController, you must set this to it
open weak var uiTextFieldDelegate: UITextFieldDelegate? open weak var uiTextFieldDelegate: UITextFieldDelegate? {
{
get { textField.delegate } get { textField.delegate }
set { set { textField.delegate = newValue }
textField.delegate = self
proprietorTextDelegate = newValue
} }
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
public var textFieldTrailingConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@objc public override init(frame: CGRect) {
super.init(frame: frame)
}
@objc public convenience init() {
self.init(frame: .zero)
}
@objc required public init?(coder: NSCoder) {
super.init(coder: coder)
fatalError("TextEntryField does not support xib.")
}
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.init(model: model, delegateObject, additionalData)
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
open override func setup() {
super.setup()
//turn off internal required rule
useRequiredRule = false
publisher(for: .valueChanged) @objc open override func setupFieldContainerContent(_ container: UIView) {
.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
textField.text = textField.text?.replacingOccurrences(of: " ", with: "")
}
}.store(in: &subscribers)
textField textField.font = Styler.Font.RegularBodyLarge.getFont()
.publisher(for: .editingDidBegin) container.addSubview(textField)
.sink { [weak self] textView in
guard let self else { return }
isEditting = true
if viewModel.clearTextOnTap {
text = ""
}
}.store(in: &subscribers)
textField NSLayoutConstraint.activate([
.publisher(for: .editingDidEnd) textField.heightAnchor.constraint(equalToConstant: Padding.Five),
.sink { [weak self] textView in textField.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three),
guard let self else { return } textField.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three),
isEditting = false container.bottomAnchor.constraint(equalTo: textField.bottomAnchor, constant: Padding.Three)
if validateWhenDoneEditing, let valid = viewModel.isValid { ])
updateValidation(valid)
}
regexTextFieldOutputIfAvailable()
}.store(in: &subscribers) textFieldTrailingConstraint = container.trailingAnchor.constraint(equalTo: textField.trailingAnchor, constant: Padding.Three)
textFieldTrailingConstraint?.isActive = true
textField.addTarget(self, action: #selector(startEditing), for: .editingDidBegin)
textField.addTarget(self, action: #selector(dismissFieldInput), for: .editingDidEnd)
let tap = UITapGestureRecognizer(target: self, action: #selector(startEditing))
entryFieldContainer.addGestureRecognizer(tap)
accessibilityElements = [textField, feedbackLabel]
} }
@objc open override func updateView(_ size: CGFloat) {
super.updateView(size)
@objc open func updateView(_ size: CGFloat) {} textField.font = Styler.Font.RegularBodyLarge.getFont()
layoutIfNeeded()
}
open override func reset() {
super.reset()
textField.isSecureTextEntry = false
textField.font = Styler.Font.RegularBodyLarge.getFont()
}
@objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) { @objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) {
observingTextFieldDelegate = delegate observingTextFieldDelegate = delegate
uiTextFieldDelegate = delegate uiTextFieldDelegate = delegate
} }
open func setupTextFieldToolbar() {
let observingDelegate = observingTextFieldDelegate ?? self
textField.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
action: #selector(observingDelegate.dismissFieldInput))
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Observing for Change (TextFieldDelegate) // MARK: - Observing for Change (TextFieldDelegate)
//-------------------------------------------------- //--------------------------------------------------
@discardableResult
@objc override open func resignFirstResponder() -> Bool {
if validateWhenDoneEditing { validateText() }
textField.resignFirstResponder()
isSelected = false
return true
}
/// Validates the text of the entry field.
@objc public override func validateText() {
text = textField.text
super.validateText()
}
/// Executes on UITextField.textDidBeginEditingNotification
@objc override func startEditing() {
super.startEditing()
if textEntryFieldModel?.clearTextOnTap ?? false {
text = ""
}
textField.becomeFirstResponder()
}
/// Executes on UITextField.textDidChangeNotification (each character entry)
@objc override func valueChanged() {
super.valueChanged()
if (textEntryFieldModel?.type == .email) {
// remove spaces (either user entered Or auto-correct suggestion) for the email field
textField.text = textField.text?.replacingOccurrences(of: " ", with: "")
}
validateText()
}
/// Executes on UITextField.textDidEndEditingNotification
@objc override func endInputing() {
super.endInputing()
// Don't show error till user starts typing.
guard text?.count ?? 0 != 0 else {
showError = false
return
}
if let isValid = textEntryFieldModel?.isValid {
self.isValid = isValid
}
regexTextFieldOutputIfAvailable()
shouldShowError(!isValid)
}
func regexTextFieldOutputIfAvailable() { func regexTextFieldOutputIfAvailable() {
if let regex = viewModel?.displayFormat, if let regex = textEntryFieldModel?.displayFormat,
let mask = viewModel?.displayMask, let mask = textEntryFieldModel?.displayMask,
let finalText = text { let finalText = text {
let range = NSRange(finalText.startIndex..., in: finalText) let range = NSRange(finalText.startIndex..., in: finalText)
@ -164,18 +291,57 @@ import VDS
} }
@objc public func dismissFieldInput(_ sender: Any?) { @objc public func dismissFieldInput(_ sender: Any?) {
_ = resignFirstResponder() resignFirstResponder()
}
open func showErrorView(_ show: Bool) {
if show {
entryFieldContainer.addSubview(errorImage)
textFieldTrailingConstraint?.isActive = false
textFieldTrailingConstraint = errorImage.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: Padding.Two)
textFieldTrailingConstraint?.isActive = true
entryFieldContainer.trailingAnchor.constraint(equalTo: errorImage.trailingAnchor, constant: Padding.Three).isActive = true
errorImage.centerYAnchor.constraint(equalTo: entryFieldContainer.centerYAnchor).isActive = true
} else {
errorImage.removeFromSuperview()
textFieldTrailingConstraint?.isActive = false
textFieldTrailingConstraint = entryFieldContainer.trailingAnchor.constraint(equalTo: textField.trailingAnchor, constant: Padding.Two)
textFieldTrailingConstraint?.isActive = true
}
}
override func shouldShowError(_ showError: Bool) {
super.shouldShowError(showError)
if showError {
observingTextFieldDelegate?.isValid?(textfield: self)
} else {
observingTextFieldDelegate?.isInvalid?(textfield: self)
}
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MoleculeViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------
open override func updateView() {
super.updateView()
if let viewModel { public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
switch viewModel.type { super.set(with: model, delegateObject, additionalData)
case .secure:
guard let model = model as? TextEntryFieldModel else { return }
self.delegateObject = delegateObject
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
text = model.text
placeholder = model.placeholder
textField.shouldMaskWhileRecording = model.shouldMaskRecordedView ?? true
textField.enableClipboardActions = model.enableClipboardActions
switch model.type {
case .password, .secure:
textField.isSecureTextEntry = true textField.isSecureTextEntry = true
textField.shouldMaskWhileRecording = true textField.shouldMaskWhileRecording = true
@ -184,167 +350,55 @@ import VDS
textField.shouldMaskWhileRecording = true textField.shouldMaskWhileRecording = true
textField.keyboardType = .numberPad textField.keyboardType = .numberPad
case .number:
textField.keyboardType = .numberPad
case .email: case .email:
textField.keyboardType = .emailAddress textField.keyboardType = .emailAddress
case .securityCode, .creditCard, .password: case .phone:
textField.shouldMaskWhileRecording = true textField.keyboardType = .phonePad
default: default:
break; textField.keyboardType = .default
} }
// Override the preset keyboard set in type. // Override the preset keyboard set in type.
if let keyboardType = viewModel.assignKeyboardType() { if let keyboardType = model.assignKeyboardType() {
textField.keyboardType = keyboardType textField.keyboardType = keyboardType
} }
}
} textField.accessibilityIdentifier = model.accessibilityIdentifier
public 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 uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate
observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate
setupTextFieldToolbar()
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected { if isSelected { startEditing() }
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 {
text = ""
viewModel.shouldClearText = false
}
updateValidation(validState)
})
}
//Added to override text when view is reloaded. //Added to override text when view is reloaded.
if let text = viewModel.text, !text.isEmpty { if let text = model.text, !text.isEmpty {
regexTextFieldOutputIfAvailable() regexTextFieldOutputIfAvailable()
} }
} setAccessibilityString(model.title ?? "")
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)
}
}
}
extension TextEntryField {
//--------------------------------------------------
// 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 // MARK: - Accessibility
extension TextEntryField { extension TextEntryField {
@objc open func pushAccessibilityNotification() { @objc open override func pushAccessibilityNotification() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
UIAccessibility.post(notification: .layoutChanged, argument: containerView) UIAccessibility.post(notification: .layoutChanged, argument: self.textField)
} }
} }
@objc open override func setAccessibilityString(_ accessibilityString: String?) {
let accessibilityString = accessibilityString ?? ""
textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
}
} }
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

@ -5,12 +5,9 @@
// 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, FormFieldInternalValidatable {
public var internalRules: [any RuleAnyModelProtocol]?
@objcMembers open class TextEntryFieldModel: EntryFieldModel {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Types // MARK: - Types
//-------------------------------------------------- //--------------------------------------------------
@ -23,39 +20,6 @@ import VDS
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
}
}
} }
//-------------------------------------------------- //--------------------------------------------------
@ -69,16 +33,12 @@ import VDS
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 = .text public var type: EntryType?
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: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -154,9 +114,6 @@ import VDS
case displayFormat case displayFormat
case displayMask case displayMask
case enableClipboardActions case enableClipboardActions
case tooltip
case transparentBackground
case width
} }
//-------------------------------------------------- //--------------------------------------------------
@ -171,7 +128,7 @@ import VDS
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) ?? .text type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type)
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
@ -192,10 +149,6 @@ import VDS
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 {
@ -211,54 +164,5 @@ import VDS
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)
} }
} }
public protocol FormFieldInternalValidatable {
var internalRules: [RuleAnyModelProtocol]? { get }
}
public class RuleAnyVDSInternalRuleModel: RuleAnyModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "anyVDSRule"
public var type: String = RuleAnyVDSInternalRuleModel.identifier
public var ruleId: String?
private var rule: VDS.AnyRule<VDS.FormFieldable>
public var errorMessage: [String: String]?
public var fields: [String]
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(fields: [String], rule: VDS.AnyRule<VDS.FormFieldable>) {
self.fields = fields
self.rule = rule
errorMessage = [:]
ruleId = "\(rule.self)"
fields.forEach {
errorMessage![$0] = rule.errorMessage
}
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
public func isValid(_ formField: FormFieldProtocol) -> Bool {
guard let field = formField as? any VDS.FormFieldable else { return true }
return rule.isValid(value: field)
}
/// never use this class as Codable
public required init(from decoder: any Decoder) throws { fatalError() }
public func encode(to encoder: any Encoder) throws {}
}

View File

@ -86,25 +86,14 @@ public extension RulesContainerProtocol {
// Validate each rule. // Validate each rule.
var valid = true var valid = true
var previousValidity: [String: FormFieldValidity] = [:] var previousValidity: [String: FormFieldValidity] = [:]
var allRules = self.rules
// append the new rules for the internal validator of any formFields
fields.compactMap({$0 as? FormFieldInternalValidatable}).forEach { field in
if let internalRules = field.internalRules {
allRules.append(contentsOf: internalRules)
}
}
fields.keys.forEach { key in fields.keys.forEach { key in
previousValidity[key] = FormFieldValidity(key) previousValidity[key] = FormFieldValidity(key)
} }
for rule in self.rules {
for rule in allRules {
//validate the rule against the fields //validate the rule against the fields
let tuple = rule.validate(fields, previousValidity) let tuple = rule.validate(fields, previousValidity)
valid = valid && tuple.valid valid = valid && tuple.valid
} }
return (valid: valid, fieldValidity: previousValidity) return (valid: valid, fieldValidity: previousValidity)
} }
} }

View File

@ -43,10 +43,10 @@ open class CoreUIModelMapping: ModelMapping {
// MARK:- Entry Field // MARK:- Entry Field
ModelRegistry.register(handler: TextEntryField.self, for: TextEntryFieldModel.self) ModelRegistry.register(handler: TextEntryField.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)
// ModelRegistry.register(handler: DateDropdownEntryField.self, for: DateDropdownEntryFieldModel.self) ModelRegistry.register(handler: DateDropdownEntryField.self, for: DateDropdownEntryFieldModel.self)
// ModelRegistry.register(handler: MultiItemDropdownEntryField.self, for: MultiItemDropdownEntryFieldModel.self) ModelRegistry.register(handler: MultiItemDropdownEntryField.self, for: MultiItemDropdownEntryFieldModel.self)
// MARK:- Selectors // MARK:- Selectors
ModelRegistry.register(handler: RadioButton.self, for: RadioButtonModel.self) ModelRegistry.register(handler: RadioButton.self, for: RadioButtonModel.self)