diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift index eb83a48a..f6f20b9f 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift @@ -18,9 +18,6 @@ import VDS public var selectedIndex: Int? public var showInlineLabel: Bool = false public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom - public var tooltip: TooltipModel? - public var transparentBackground: Bool = false - public var width: CGFloat? public init(with options: [String], selectedIndex: Int? = nil) { self.options = options @@ -51,9 +48,6 @@ import VDS case action case showInlineLabel case feedbackTextPlacement - case tooltip - case transparentBackground - case width } //-------------------------------------------------- @@ -73,9 +67,6 @@ import VDS 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) - 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) } public override func encode(to encoder: Encoder) throws { @@ -86,8 +77,5 @@ import VDS try container.encode(showInlineLabel, forKey: .showInlineLabel) try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement) try container.encodeModelIfPresent(action, forKey: .action) - try container.encodeIfPresent(tooltip, forKey: .tooltip) - try container.encode(transparentBackground, forKey: .transparentBackground) - try container.encodeIfPresent(width, forKey: .width) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift index 46e4e233..1cacbeae 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift @@ -7,100 +7,60 @@ // import UIKit +import VDS - -class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate { - //-------------------------------------------------- - // MARK: - Outlets - //-------------------------------------------------- - - open private(set) var textView: TextView = { - let textView = TextView() - textView.setContentCompressionResistancePriority(.required, for: .vertical) - return textView - }() - - //-------------------------------------------------- +open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol { + //------------------------------------------------------ // MARK: - Properties - //-------------------------------------------------- + //------------------------------------------------------ + open var viewModel: TextViewEntryFieldModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + open var fieldKey: String? + open var fieldValue: JSONValue? + open var groupName: String? - private var observingForChange: Bool = false + //-------------------------------------------------- + // MARK: - Stored Properties + //-------------------------------------------------- + public var isValid: Bool = true + + private var isEditting: Bool = false { + didSet { + viewModel.selected = isEditting + } + } //-------------------------------------------------- // MARK: - Computed Properties //-------------------------------------------------- - - public var textViewEntryFieldModel: TextViewEntryFieldModel? { - model as? TextViewEntryFieldModel + open var shouldMaskWhileRecording: Bool { + return viewModel.shouldMaskRecordedView ?? false } - - public override var isEnabled: Bool { - get { super.isEnabled } - set (enabled) { - super.isEnabled = enabled - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.textView.isEnabled = enabled - if self.textView.isShowingPlaceholder { - self.textView.textColor = self.textView.placeholderTextColor - } else { - self.textView.textColor = (self.textView.isEnabled ? self.textViewEntryFieldModel?.enabledTextColor : self.textViewEntryFieldModel?.disabledTextColor)?.uiColor - } - } - } - } - - public override var showError: Bool { - get { super.showError } - set (error) { - - if error { - textView.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "") - } else { - textView.accessibilityValue = nil - } - - super.showError = error + + /// Placeholder access for the textView. + open var placeholder: String? { + get { viewModel?.placeholder } + set { + textView.placeholder = newValue ?? "" + viewModel?.placeholder = newValue } } /// The text of this textView. open override var text: String? { - get { textViewEntryFieldModel?.text } - set { - textView.text = newValue - textViewEntryFieldModel?.text = newValue + didSet { + viewModel?.text = text } } - /// Placeholder access for the textView. - public var placeholder: String? { - get { textViewEntryFieldModel?.placeholder } - set { - textView.placeholder = newValue ?? "" - textViewEntryFieldModel?.placeholder = newValue - textView.setPlaceholderIfAvailable() + open override var errorText: String? { + get { + viewModel.dynamicErrorMessage ?? viewModel.errorMessage } - } - - //-------------------------------------------------- - // MARK: - Constraint - //-------------------------------------------------- - - public var heightConstraint: NSLayoutConstraint? - private var topConstraint: NSLayoutConstraint? - private var leadingConstraint: NSLayoutConstraint? - private var trailingConstraint: NSLayoutConstraint? - private var bottomConstraint: NSLayoutConstraint? - - private func adjustMarginConstraints(constant: CGFloat) { - - topConstraint?.constant = constant - leadingConstraint?.constant = constant - trailingConstraint?.constant = constant - bottomConstraint?.constant = constant + set {} } //-------------------------------------------------- @@ -108,198 +68,178 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele //-------------------------------------------------- /// The delegate and block for validation. Validates if the text that the user has entered. - public weak var observingTextViewDelegate: ObservingTextFieldDelegate? { - didSet { - if observingTextViewDelegate != nil && !observingForChange { - observingForChange = true - NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextView.textDidChangeNotification, object: textView) - NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextView.textDidEndEditingNotification, object: textView) - NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextView.textDidBeginEditingNotification, object: textView) - - } else if observingTextViewDelegate == nil && observingForChange { - observingForChange = false - NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: textView) - NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: textView) - NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: textView) - } - } - } + open weak var observingTextViewDelegate: ObservingTextFieldDelegate? /// If you're using a ViewController, you must set this to it - public weak var uiTextViewDelegate: UITextViewDelegate? { + open weak var uiTextViewDelegate: UITextViewDelegate? { get { textView.delegate } set { textView.delegate = newValue } } - @objc public func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) { + @objc open func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) { observingTextViewDelegate = delegate uiTextViewDelegate = delegate } - open func setupTextViewToolbar() { - let observingDelegate = observingTextViewDelegate ?? self - textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate, - action: #selector(observingDelegate.dismissFieldInput)) - } - //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- - - @objc open override func setupFieldContainerContent(_ container: UIView) { + open override func setup() { + super.setup() + //turn off internal required rule + useRequiredRule = false - container.addSubview(textView) + publisher(for: .valueChanged) + .sink { [weak self] control in + guard let self else { return } + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + }.store(in: &subscribers) - topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three) - leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three) - trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three) - bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three) + textView + .publisher(for: .editingDidBegin) + .sink { [weak self] textView in + guard let self else { return } + isEditting = true + + }.store(in: &subscribers) - topConstraint?.isActive = true - leadingConstraint?.isActive = true - trailingConstraint?.isActive = true - bottomConstraint?.isActive = true - - heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0) - accessibilityElements = [textView] + textView + .publisher(for: .editingDidEnd) + .sink { [weak self] textView in + guard let self else { return } + isEditting = false + if let valid = viewModel.isValid { + updateValidation(valid) + } + + }.store(in: &subscribers) } - - open override func updateView(_ size: CGFloat) { - super.updateView(size) - textView.updateView(size) - } - - open override func reset() { - super.reset() - textView.reset() - adjustMarginConstraints(constant: Padding.Three) - heightConstraint?.constant = 0 - heightConstraint?.isActive = false - } + open func viewModelDidUpdate() { - //-------------------------------------------------- - // MARK: - Methods - //-------------------------------------------------- - - /// Validates the text of the entry field. - @objc public override func validateText() { - text = textView.text - super.validateText() - } - - /// Executes on UITextView.textDidBeginEditingNotification - @objc override func startEditing() { - super.startEditing() - _ = textView.becomeFirstResponder() - } - - /// Executes on UITextView.textDidChangeNotification (each character entry) - @objc override func valueChanged() { - super.valueChanged() - validateText() - } - - /// Executes on UITextView.textDidEndEditingNotification - @objc override func endInputing() { - super.endInputing() + text = viewModel.text + minHeight = viewModel.minHeight + maxLength = viewModel.maxLength - // Don't show error till user starts typing. - guard text?.count ?? 0 != 0 else { - showError = false - return - } - - if let isValid = textViewEntryFieldModel?.isValid { - self.isValid = isValid - } - - showError = !isValid - } - - //-------------------------------------------------- - // MARK: - MoleculeViewProtocol - //-------------------------------------------------- - - open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - - guard let model = model as? TextViewEntryFieldModel else { return } - - if let height = model.height { - heightConstraint?.constant = height - heightConstraint?.isActive = true - } - - text = model.text + labelText = viewModel.title + helperText = viewModel.feedback + isEnabled = viewModel.enabled + isReadOnly = viewModel.readOnly + isRequired = viewModel.required + tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + width = viewModel.width + transparentBackground = viewModel.transparentBackground + uiTextViewDelegate = delegateObject?.uiTextViewDelegate observingTextViewDelegate = delegateObject?.observingTextFieldDelegate - if let accessibilityText = model.accessibilityText { + if let accessibilityText = viewModel.accessibilityText { accessibilityLabel = accessibilityText } + containerView.accessibilityIdentifier = viewModel.accessibilityIdentifier + textView.isEditable = viewModel.editable + textView.textAlignment = viewModel.textAlignment + textView.placeholder = viewModel.placeholder ?? "" + + if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected { + + viewModel.wasInitiallySelected = true + isEditting = true + } - textView.isEditable = model.editable - textView.textAlignment = model.textAlignment - textView.accessibilityIdentifier = model.accessibilityIdentifier - textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor - textView.font = model.fontStyle.getFont() - textView.placeholder = model.placeholder ?? "" - textView.placeholderFontStyle = model.placeholderFontStyle - textView.placeholderTextColor = model.placeholderTextColor.uiColor - textView.setPlaceholderIfAvailable() - - switch model.type { - case .secure, .password: + switch viewModel.type { + case .secure: textView.isSecureTextEntry = true + textView.shouldMaskWhileRecording = true case .numberSecure: textView.isSecureTextEntry = true - textView.keyboardType = .numberPad - - case .number: + textView.shouldMaskWhileRecording = true textView.keyboardType = .numberPad case .email: textView.keyboardType = .emailAddress - default: break + case .securityCode, .creditCard, .password: + textView.shouldMaskWhileRecording = true + + default: + break; } + // Override the preset keyboard set in type. + if let keyboardType = viewModel.assignKeyboardType() { + textView.keyboardType = keyboardType + } + + /// append any internal rules: + viewModel.rules = rules + /// No point in configuring if the TextView is Read-only. if textView.isEditable { - FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) - setupTextViewToolbar() + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) - if isSelected { + if isEditting { DispatchQueue.main.async { - _ = self.textView.becomeFirstResponder() + _ = self.becomeFirstResponder() } } } - if model.hideBorders { - adjustMarginConstraints(constant: 0) + viewModel.updateUI = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + if isEditting { + updateValidation(viewModel.isValid ?? true) + + } else if viewModel.isValid ?? true && showError { + showError = false + } + isEnabled = viewModel.enabled + }) } - updateAccessibility(model: model) + + viewModel.updateUIDynamicError = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + let validState = viewModel.isValid ?? false + if !validState && viewModel.shouldClearText { + text = "" + viewModel.shouldClearText = false + } + updateValidation(validState) + }) + } + } - func updateAccessibility(model: TextViewEntryFieldModel) { + private func updateValidation(_ isValid: Bool) { + let previousValidity = self.isValid + self.isValid = isValid - var message = "" - - if let titleText = model.accessibilityText ?? model.title { - message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" + if previousValidity && !isValid { + showError = true + } else if (!previousValidity && isValid) { + showError = false } - - if let feedback = model.feedback { - message += ", " + feedback + } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + open func updateView(_ size: CGFloat) {} +} + +extension VDS.TextView: ViewMaskingProtocol { + public var shouldMaskWhileRecording: Bool { + get { + return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false } - - if let errorMessage = errorLabel.text { - message += ", " + errorMessage + set { + objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - - textView.accessibilityLabel = message } } + diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift index 42da3ca2..2f47b2a6 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift @@ -7,9 +7,9 @@ // import UIKit +import VDS - -class TextViewEntryFieldModel: TextEntryFieldModel { +public class TextViewEntryFieldModel: TextEntryFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -17,24 +17,20 @@ class TextViewEntryFieldModel: TextEntryFieldModel { public override class var identifier: String { "textView" } public var accessibilityText: String? - public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge - public var height: CGFloat? - public var placeholderTextColor: Color = Color(uiColor: .mvmCoolGray3) - public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro public var editable: Bool = true public var showsPlaceholder: Bool = false - + public var minHeight: VDS.TextArea.Height = .twoX + public var maxLength: Int? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { case accessibilityText - case fontStyle - case height - case placeholderFontStyle - case placeholderTextColor case editable + case minHeight + case maxLength } //-------------------------------------------------- @@ -45,34 +41,18 @@ class TextViewEntryFieldModel: TextEntryFieldModel { try super.init(from: decoder) let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - if let placeholderFontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .placeholderFontStyle) { - self.placeholderFontStyle = placeholderFontStyle - } - - if let placeholderTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .placeholderTextColor) { - self.placeholderTextColor = placeholderTextColor - } - - if let fontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .fontStyle) { - self.fontStyle = fontStyle - } - - if let editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) { - self.editable = editable - } - + editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) ?? true + minHeight = try typeContainer.decodeIfPresent(VDS.TextArea.Height.self, forKey: .minHeight) ?? .twoX + maxLength = try typeContainer.decodeIfPresent(Int.self, forKey: .maxLength) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) - height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) } public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) - try container.encodeIfPresent(height, forKey: .height) - try container.encode(fontStyle, forKey: .fontStyle) try container.encode(editable, forKey: .editable) - try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle) - try container.encode(placeholderTextColor, forKey: .placeholderTextColor) + try container.encode(minHeight, forKey: .minHeight) + try container.encodeIfPresent(maxLength, forKey: .maxLength) } } diff --git a/MVMCoreUI/FormUIHelpers/FormValidator.swift b/MVMCoreUI/FormUIHelpers/FormValidator.swift index b01c7ad4..e68b87a9 100644 --- a/MVMCoreUI/FormUIHelpers/FormValidator.swift +++ b/MVMCoreUI/FormUIHelpers/FormValidator.swift @@ -56,6 +56,12 @@ import MVMCore //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 diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift index e4cfb2aa..5443cda4 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift @@ -9,12 +9,15 @@ import Foundation import VDS -open class VDSRuleBase: RuleAnyModelProtocol { +open class VDSRuleBase: RuleAnyModelProtocol { open var ruleId: String? + open var ruleType: String open var errorMessage: [String : String]? open var fields = [String]() - public init(){} - + public init(){ + ruleType = Self.identifier + } + public var type: String { ruleType } open func isValid(_ formField: any FormFieldProtocol) -> Bool { fatalError() } @@ -26,7 +29,7 @@ public class RuleVDSModel: VDSRuleBase { // MARK: - Properties //-------------------------------------------------- public var rule: AnyRule - + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -34,8 +37,8 @@ public class RuleVDSModel: VDSRuleBase { public init(field: String, rule: AnyRule) { self.rule = rule super.init() + self.ruleType = rule.ruleType self.fields = [field] - self.ruleId = "\(rule.self)-\(Int.random(in: 1...1000))" } required init(from decoder: any Decoder) throws { @@ -57,3 +60,4 @@ public class RuleVDSModel: VDSRuleBase { return valid } } +