diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 39bd239c..d69d4d9a 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -578,6 +578,7 @@ EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; }; EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; }; EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; }; + EA1B02DA2C407BD600F0758B /* LegacyTextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02D92C407BD600F0758B /* LegacyTextEntryField.swift */; }; EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; @@ -1199,6 +1200,7 @@ EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = ""; }; EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = ""; }; EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; + EA1B02D92C407BD600F0758B /* LegacyTextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTextEntryField.swift; sourceTree = ""; }; EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = ""; }; EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = ""; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; @@ -2350,6 +2352,7 @@ 0A21DB7E235DECC500C160A2 /* EntryField.swift */, 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, + EA1B02D92C407BD600F0758B /* LegacyTextEntryField.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */, 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */, @@ -2845,6 +2848,7 @@ 942C378C2412F4FA0066E45E /* ModalMoleculeListTemplate.swift in Sources */, BB47A588241615FA002BB23C /* ListOneColumnFullWidthTextDividerSubsection.swift in Sources */, 012A88C8238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift in Sources */, + EA1B02DA2C407BD600F0758B /* LegacyTextEntryField.swift in Sources */, 8D8067D12444472F00203BE8 /* ListRightVariablePriceChangeAllTextAndLinksModel.swift in Sources */, 0A7EF86123D8AC2500B2AAD1 /* DigitEntryFieldModel.swift in Sources */, D224798C231450C8003FCCF9 /* HeadlineBodyToggle.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift index 2e098ad9..c4bf1c1f 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift @@ -11,7 +11,7 @@ import UIKit /** * Subclass of TextEntryField as it is to use similar logic as a singular textField but appear separate.. */ -@objcMembers open class DigitEntryField: TextEntryField, DigitBoxProtocol { +@objcMembers open class DigitEntryField: LegacyTextEntryField, DigitBoxProtocol { //-------------------------------------------------- // MARK: - Stored Properties //-------------------------------------------------- @@ -345,9 +345,9 @@ import UIKit numberOfDigits = model.digits - if let entryType = model.type { - setAsSecureTextEntry(entryType == .secure || entryType == .password) - } + let entryType = model.type + setAsSecureTextEntry(entryType == .secure || entryType == .password) + let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift index 96bb8acf..6940371b 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift @@ -12,7 +12,7 @@ import MVMCore This class is intended to be subclassed. See ItemDropdownEntryField and DateDropdownEntryField. */ -@objcMembers open class BaseDropdownEntryField: TextEntryField { +@objcMembers open class BaseDropdownEntryField: LegacyTextEntryField { //-------------------------------------------------- // MARK: - Outlets //-------------------------------------------------- @@ -48,7 +48,10 @@ import MVMCore //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- - + @objc public convenience init() { + self.init(frame: .zero) + } + @objc public override init(frame: CGRect) { super.init(frame: frame) } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index 910712d5..7a5ee9ed 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -47,51 +47,9 @@ import MVMCore get { MVMCoreUIUtility.removeMdnFormat(text) } set { text = MVMCoreUIUtility.formatMdn(newValue) } } - - /// Toggles selected or original (unselected) UI. - public override var isSelected: Bool { - get { return entryFieldContainer.isSelected } - set (selected) { - if selected && showError { - showError = false - } - - super.isSelected = selected - } - } - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - - @objc public override init(frame: CGRect) { - super.init(frame: .zero) - } - - @objc public convenience init() { - self.init(frame: .zero) - } - - @objc required public init?(coder: NSCoder) { - super.init(coder: coder) - fatalError("MdnEntryField xib not supported.") - } - - required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.init(model: model, delegateObject, additionalData) - } - - //-------------------------------------------------- - // MARK: - Setup - //-------------------------------------------------- - - @objc public override func setupFieldContainerContent(_ container: UIView) { - super.setupFieldContainerContent(container) - - textField.keyboardType = .numberPad - } - - open override func setupTextFieldToolbar() { + + open override func setup() { + super.setup() let toolbar = UIToolbar.createEmptyToolbar() let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) @@ -99,12 +57,17 @@ import MVMCore let dismissButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissFieldInput)) toolbar.items = [contacts, space, dismissButton] textField.inputAccessoryView = toolbar + } + open override var viewModel: TextEntryFieldModel! { + didSet { + viewModel.type = .phone + } + } //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- - @objc public func hasValidMDN() -> Bool { guard let MDN = mdn, !MDN.isEmpty else { return false } @@ -129,14 +92,14 @@ import MVMCore showError = false } else { - entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message") + viewModel?.errorMessage = viewModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message") showError = true UIAccessibility.post(notification: .layoutChanged, argument: textField) } return isValid } - + @objc public func getContacts(_ sender: Any?) { let picker = CNContactPickerViewController() @@ -144,20 +107,15 @@ import MVMCore picker.displayedPropertyKeys = ["phoneNumbers"] picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0") picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'") - Task(priority: .userInitiated) { - await NavigationHandler.shared().present(viewController: picker, animated: true) + if let topViewController = UIApplication.topViewController() { + topViewController.present(picker, animated: true) } } //-------------------------------------------------- // 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 @@ -168,21 +126,11 @@ import MVMCore if let phoneNumber = contactProperty.value as? CNPhoneNumber { let MDN = phoneNumber.stringValue - 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...]) - } - + let unformattedMDN = MVMCoreUIUtility.removeMdnFormat(MDN) text = unformattedMDN textFieldShouldReturn(textField) textFieldDidEndEditing(textField) + _ = resignFirstResponder() } } @@ -190,51 +138,49 @@ import MVMCore // MARK: - Implemented TextField Delegate //-------------------------------------------------- - @discardableResult - @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - - textField.resignFirstResponder() - - return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true - } - - @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - - if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) { - return false - } - - return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true - } - - @objc public func textFieldDidBeginEditing(_ textField: UITextField) { - - textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text) - proprietorTextDelegate?.textFieldDidBeginEditing?(textField) - } - - @objc public func textFieldDidEndEditing(_ textField: UITextField) { - - proprietorTextDelegate?.textFieldDidEndEditing?(textField) - - if validateMDNTextField() { - if isNationalMDN { - textField.text = MVMCoreUIUtility.formatMdn(textField.text) - } - // Validate the base input field along with triggering form field validation rules. - validateText() - } - } - - @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 - } +// @discardableResult +// @objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool { +// +// textField.resignFirstResponder() +// +// return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true +// } +// +// @objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { +// +// if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) { +// return false +// } +// +// return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true +// } +// +// @objc public override func textFieldDidBeginEditing(_ textField: UITextField) { +// +// textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text) +// proprietorTextDelegate?.textFieldDidBeginEditing?(textField) +// } +// +// @objc public override func textFieldDidEndEditing(_ textField: UITextField) { +// +// proprietorTextDelegate?.textFieldDidEndEditing?(textField) +// +// if validateMDNTextField() { +// if isNationalMDN { +// textField.text = MVMCoreUIUtility.formatMdn(textField.text) +// } +// } +// } +// +// @objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { +// proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true +// } +// +// @objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { +// proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true +// } +// +// @objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool { +// proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true +// } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index 9d030d0f..ac942876 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -7,44 +7,46 @@ // import UIKit - +import VDS @objc public protocol ObservingTextFieldDelegate { /// Called when the entered text becomes valid based on the validation block - @objc optional func isValid(textfield: TextEntryField?) + @objc optional func isValid(textfield: EntryField?) /// Called when the entered text becomes invalid based on the validation block - @objc optional func isInvalid(textfield: TextEntryField?) + @objc optional func isInvalid(textfield: EntryField?) /// Dismisses the keyboard. @objc optional func dismissFieldInput(_ sender: Any?) } -@objcMembers open class TextEntryField: EntryField, UITextFieldDelegate, ObservingTextFieldDelegate { +@objcMembers open class TextEntryField: 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: - Outlets + // MARK: - Stored Properties //-------------------------------------------------- - - open private(set) var textField: TextField = { - 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 - }() - - public lazy var errorImage: UIImageView = { - let image = MVMCoreUIUtility.imageNamed("alert_standard") - let imageView = UIImageView(image: image) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true - imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true - return imageView - }() - + 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 //-------------------------------------------------- @@ -52,52 +54,19 @@ import UIKit private var observingForChange: Bool = false /// Validate when user resigns editing. Default: true - public var validateWhenDoneEditing: Bool = true - - public var textEntryFieldModel: TextEntryFieldModel? { model as? TextEntryFieldModel } + open var validateWhenDoneEditing: Bool = true + open var shouldMaskWhileRecording: Bool { + return viewModel.shouldMaskRecordedView ?? false + } //-------------------------------------------------- // 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. open override var text: String? { - get { textField.text } - set { - textEntryFieldModel?.text = newValue - textField.text = newValue + didSet { + viewModel?.text = text } } @@ -110,173 +79,77 @@ import UIKit //-------------------------------------------------- // MARK: - Delegate Properties //-------------------------------------------------- - /// The delegate and block for validation. Validates if the text that the user has entered. - 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) - } - } - } + public weak var observingTextFieldDelegate: ObservingTextFieldDelegate? /// 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 } - set { textField.delegate = 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) + set { + textField.delegate = self + proprietorTextDelegate = newValue + } } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- - - @objc open override func setupFieldContainerContent(_ container: UIView) { + open override func setup() { + super.setup() + //turn off internal required rule + useRequiredRule = false - textField.font = Styler.Font.RegularBodyLarge.getFont() - container.addSubview(textField) + 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 + textField.text = textField.text?.replacingOccurrences(of: " ", with: "") + } + }.store(in: &subscribers) - NSLayoutConstraint.activate([ - textField.heightAnchor.constraint(equalToConstant: Padding.Five), - textField.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three), - textField.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three), - container.bottomAnchor.constraint(equalTo: textField.bottomAnchor, constant: Padding.Three) - ]) + textField + .publisher(for: .editingDidBegin) + .sink { [weak self] textView in + guard let self else { return } + isEditting = true + if viewModel.clearTextOnTap { + text = "" + } + }.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) - - textField.font = Styler.Font.RegularBodyLarge.getFont() - layoutIfNeeded() - } - - open override func reset() { - super.reset() - - textField.isSecureTextEntry = false - textField.font = Styler.Font.RegularBodyLarge.getFont() + 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) + } + + @objc open func updateView(_ size: CGFloat) {} + @objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) { observingTextFieldDelegate = 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) //-------------------------------------------------- - - @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() { - if let regex = textEntryFieldModel?.displayFormat, - let mask = textEntryFieldModel?.displayMask, + if let regex = viewModel?.displayFormat, + let mask = viewModel?.displayMask, let finalText = text { let range = NSRange(finalText.startIndex..., in: finalText) @@ -291,114 +164,187 @@ import UIKit } @objc public func dismissFieldInput(_ sender: Any?) { - 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) - } + _ = resignFirstResponder() } //-------------------------------------------------- // MARK: - MoleculeViewProtocol //-------------------------------------------------- - - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) + open override func updateView() { + super.updateView() - 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.shouldMaskWhileRecording = true + 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; + } - case .numberSecure: - textField.isSecureTextEntry = true - textField.shouldMaskWhileRecording = true - textField.keyboardType = .numberPad - - case .number: - textField.keyboardType = .numberPad - - case .email: - textField.keyboardType = .emailAddress - - case .phone: - textField.keyboardType = .phonePad - - default: - textField.keyboardType = .default + // Override the preset keyboard set in type. + if let keyboardType = viewModel.assignKeyboardType() { + textField.keyboardType = keyboardType + } } + + } + public func viewModelDidUpdate() { - // Override the preset keyboard set in type. - if let keyboardType = model.assignKeyboardType() { - textField.keyboardType = keyboardType - } + fieldType = viewModel.type.toVDSFieldType() + text = viewModel.text + placeholder = viewModel.placeholder - textField.accessibilityIdentifier = model.accessibilityIdentifier + 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 - setupTextFieldToolbar() - if isSelected { startEditing() } + 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 { + text = "" + viewModel.shouldClearText = false + } + updateValidation(validState) + }) + } //Added to override text when view is reloaded. - if let text = model.text, !text.isEmpty { + if let text = viewModel.text, !text.isEmpty { 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 extension TextEntryField { - @objc open override func pushAccessibilityNotification() { + @objc open func pushAccessibilityNotification() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - UIAccessibility.post(notification: .layoutChanged, argument: self.textField) + UIAccessibility.post(notification: .layoutChanged, argument: containerView) } } - - @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) + } } } + diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift index 053a5ac5..49204233 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift @@ -5,9 +5,12 @@ // Created by Kevin Christiano on 1/22/20. // Copyright © 2020 Verizon Wireless. All rights reserved. // +import VDS - -@objcMembers open class TextEntryFieldModel: EntryFieldModel { +@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatable { + + public var internalRules: [any RuleAnyModelProtocol]? + //-------------------------------------------------- // MARK: - Types //-------------------------------------------------- @@ -20,6 +23,39 @@ case email case text case phone + + //additional + case inlineAction + case creditCard + case date + case securityCode + + public func toVDSFieldType() -> VDS.InputField.FieldType { + switch self { + case .password: + .password + case .secure: + .text + case .number: + .number + case .numberSecure: + .number + case .email: + .text + case .text: + .text + case .phone: + .telephone + case .inlineAction: + .inlineAction + case .creditCard: + .creditCard + case .date: + .date + case .securityCode: + .securityCode + } + } } //-------------------------------------------------- @@ -33,12 +69,16 @@ public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3) public var textAlignment: NSTextAlignment = .left public var keyboardOverride: String? - public var type: EntryType? + public var type: EntryType = .text public var clearTextOnTap: Bool = false public var displayFormat: String? public var displayMask: String? public var enableClipboardActions: Bool = true + public var tooltip: TooltipModel? + public var transparentBackground: Bool = false + public var width: CGFloat? + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -114,6 +154,9 @@ case displayFormat case displayMask case enableClipboardActions + case tooltip + case transparentBackground + case width } //-------------------------------------------------- @@ -128,7 +171,7 @@ displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat) keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride) displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask) - type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) + type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) ?? .text if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) { self.clearTextOnTap = clearTextOnTap @@ -149,6 +192,10 @@ if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) { self.enableClipboardActions = enableClipboardActions } + + tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip) + transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false + width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width) } open override func encode(to encoder: Encoder) throws { @@ -164,5 +211,54 @@ try container.encode(disabledTextColor, forKey: .disabledTextColor) try container.encode(clearTextOnTap, forKey: .clearTextOnTap) try container.encode(enableClipboardActions, forKey: .enableClipboardActions) + try container.encodeIfPresent(tooltip, forKey: .tooltip) + try container.encode(transparentBackground, forKey: .transparentBackground) + try container.encodeIfPresent(width, forKey: .width) } } + +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 + public var errorMessage: [String: String]? + public var fields: [String] + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(fields: [String], rule: VDS.AnyRule) { + 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 {} +} + diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift index d1326ba9..1c4c7271 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift @@ -86,14 +86,25 @@ public extension RulesContainerProtocol { // Validate each rule. var valid = true 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 previousValidity[key] = FormFieldValidity(key) } - for rule in self.rules { + + for rule in allRules { //validate the rule against the fields let tuple = rule.validate(fields, previousValidity) valid = valid && tuple.valid } + return (valid: valid, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 22f65ade..fcba8f37 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -43,10 +43,10 @@ open class CoreUIModelMapping: ModelMapping { // MARK:- Entry Field ModelRegistry.register(handler: TextEntryField.self, for: TextEntryFieldModel.self) ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self) - ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self) - ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self) - ModelRegistry.register(handler: DateDropdownEntryField.self, for: DateDropdownEntryFieldModel.self) - ModelRegistry.register(handler: MultiItemDropdownEntryField.self, for: MultiItemDropdownEntryFieldModel.self) + //ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self) +// ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self) +// ModelRegistry.register(handler: DateDropdownEntryField.self, for: DateDropdownEntryFieldModel.self) +// ModelRegistry.register(handler: MultiItemDropdownEntryField.self, for: MultiItemDropdownEntryFieldModel.self) // MARK:- Selectors ModelRegistry.register(handler: RadioButton.self, for: RadioButtonModel.self)