From a4b550cf0394cf811931f44a90f2e7a4ed6d100f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 11 Jul 2024 10:19:01 -0500 Subject: [PATCH] first cut of textEntry Signed-off-by: Matt Bruce --- .../TextFields/TextViewEntryField.swift | 316 ++++++------------ .../TextFields/TextViewEntryFieldModel.swift | 29 +- 2 files changed, 113 insertions(+), 232 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift index 46e4e233..415f1bb4 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift @@ -7,123 +7,63 @@ // 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 - }() - - //-------------------------------------------------- +class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate { + //------------------------------------------------------ // MARK: - Properties - //-------------------------------------------------- + //------------------------------------------------------ + open var viewModel: TextViewEntryFieldModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + var fieldKey: String? + var fieldValue: JSONValue? + 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 - } - - 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 - } - } - /// 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 } + get { viewModel?.placeholder } set { textView.placeholder = newValue ?? "" - textViewEntryFieldModel?.placeholder = newValue - textView.setPlaceholderIfAvailable() + viewModel?.placeholder = newValue } } - //-------------------------------------------------- - // 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 + override var errorText: String? { + get { + viewModel.dynamicErrorMessage ?? viewModel.errorMessage + } + set {} } - //-------------------------------------------------- // MARK: - Delegate Properties //-------------------------------------------------- /// The delegate and block for validation. Validates if the text that the user has entered. - public weak var 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) - } - } - } + public weak var observingTextViewDelegate: ObservingTextFieldDelegate? /// If you're using a ViewController, you must set this to it public weak var uiTextViewDelegate: UITextViewDelegate? { @@ -136,102 +76,49 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele 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) { + override func setup() { + super.setup() - 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 { + isValid = valid + } + showError = !isValid + + }.store(in: &subscribers) + + //String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "") } - - 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 - } - - //-------------------------------------------------- - // 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() - - // 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 func updateView(_ size: CGFloat) {} + + open func viewModelDidUpdate() { - 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 + text = viewModel.text uiTextViewDelegate = delegateObject?.uiTextViewDelegate observingTextViewDelegate = delegateObject?.observingTextFieldDelegate @@ -239,17 +126,12 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele accessibilityLabel = accessibilityText } - textView.isEditable = model.editable - textView.textAlignment = model.textAlignment + textView.isEditable = viewModel.editable + textView.textAlignment = viewModel.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() + textView.placeholder = viewModel.placeholder ?? "" - switch model.type { + switch viewModel.type { case .secure, .password: textView.isSecureTextEntry = true @@ -266,40 +148,60 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele default: break } + if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected { + + viewModel.wasInitiallySelected = true + isEditting = true + } + /// 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 - } - - if let errorMessage = errorLabel.text { - message += ", " + errorMessage - } - - textView.accessibilityLabel = message } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift index 42da3ca2..fa941d10 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift @@ -17,12 +17,9 @@ 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 tooltip: TooltipModel? //-------------------------------------------------- // MARK: - Keys @@ -30,11 +27,8 @@ class TextViewEntryFieldModel: TextEntryFieldModel { private enum CodingKeys: String, CodingKey { case accessibilityText - case fontStyle - case height - case placeholderFontStyle - case placeholderTextColor case editable + case tooltip } //-------------------------------------------------- @@ -45,34 +39,19 @@ 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 } accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) - height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) + tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip) } 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.encodeIfPresent(tooltip, forKey: .tooltip) } }