diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 8b677888..a6a519ed 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -68,6 +68,8 @@ 0A21DB83235DFBC500C160A2 /* MdnEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */; }; 0A21DB91235E0EDB00C160A2 /* DigitBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */; }; 0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */; }; + 0A25209624645AFD000FA9F6 /* TextViewEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A25209524645AFD000FA9F6 /* TextViewEntryField.swift */; }; + 0A25209824645B76000FA9F6 /* TextViewEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A25209724645B76000FA9F6 /* TextViewEntryFieldModel.swift */; }; 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */; }; 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */; }; 0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */; }; @@ -76,7 +78,6 @@ 0A6682AA2435125F00AD3CA1 /* Styler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6682A92435125F00AD3CA1 /* Styler.swift */; }; 0A6682AC243531C300AD3CA1 /* Padding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6682AB243531C300AD3CA1 /* Padding.swift */; }; 0A6682B5243769C700AD3CA1 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6682B3243769C700AD3CA1 /* TextView.swift */; }; - 0A6682B6243769C700AD3CA1 /* TextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6682B4243769C700AD3CA1 /* TextViewModel.swift */; }; 0A69F611241BDEA700F7231B /* RuleAnyRequiredModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A69F610241BDEA700F7231B /* RuleAnyRequiredModel.swift */; }; 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */; }; 0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; }; @@ -479,6 +480,8 @@ 0A21DB7E235DECC500C160A2 /* EntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = ""; }; 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnEntryField.swift; sourceTree = ""; }; 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitEntryField.swift; sourceTree = ""; }; + 0A25209524645AFD000FA9F6 /* TextViewEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewEntryField.swift; sourceTree = ""; }; + 0A25209724645B76000FA9F6 /* TextViewEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewEntryFieldModel.swift; sourceTree = ""; }; 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = ""; }; 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesProtocol.swift; sourceTree = ""; }; @@ -487,7 +490,6 @@ 0A6682A92435125F00AD3CA1 /* Styler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styler.swift; sourceTree = ""; }; 0A6682AB243531C300AD3CA1 /* Padding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Padding.swift; sourceTree = ""; }; 0A6682B3243769C700AD3CA1 /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; - 0A6682B4243769C700AD3CA1 /* TextViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewModel.swift; sourceTree = ""; }; 0A69F610241BDEA700F7231B /* RuleAnyRequiredModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyRequiredModel.swift; sourceTree = ""; }; 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = ""; }; 0A7918F423F5E7EA00772FF4 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; @@ -1650,6 +1652,8 @@ 0ABD136C237CAD1E0081388D /* DateDropdownEntryField.swift */, 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */, 0ABD1370237DB0450081388D /* ItemDropdownEntryField.swift */, + 0A25209724645B76000FA9F6 /* TextViewEntryFieldModel.swift */, + 0A25209524645AFD000FA9F6 /* TextViewEntryField.swift */, ); path = TextFields; sourceTree = ""; @@ -1760,7 +1764,6 @@ D2B18B7D236090D500A9AEDC /* BaseClasses */ = { isa = PBXGroup; children = ( - 0A6682B4243769C700AD3CA1 /* TextViewModel.swift */, 0A6682B3243769C700AD3CA1 /* TextView.swift */, C003506023AA94CD00B6AC29 /* Button.swift */, D2B18B7E2360913400A9AEDC /* Control.swift */, @@ -2038,6 +2041,7 @@ 943820842432382400B43AF3 /* WebView.swift in Sources */, 0103B84E23D7E33A009C315C /* HeadlineBodyToggleModel.swift in Sources */, D2755D7B23689C7500485468 /* TableViewCell.swift in Sources */, + 0A25209624645AFD000FA9F6 /* TextViewEntryField.swift in Sources */, 014AA72623C501E2006F3E93 /* ContainerModelProtocol.swift in Sources */, AA11A42123F15D7000D7962F /* ListRightVariablePaymentsModel.swift in Sources */, 011D9626240EBB16000E3791 /* RadioButtonLabelModel.swift in Sources */, @@ -2150,7 +2154,6 @@ 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */, D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */, - 0A6682B6243769C700AD3CA1 /* TextViewModel.swift in Sources */, 9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */, 011D95A5240455DC000E3791 /* FormGroupRule.swift in Sources */, D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */, @@ -2272,6 +2275,7 @@ AA26850C244840AE00CE34CC /* HeadersH2TinyButton.swift in Sources */, 011D95AB2405C553000E3791 /* FormItemProtocol.swift in Sources */, D21EE53C23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift in Sources */, + 0A25209824645B76000FA9F6 /* TextViewEntryFieldModel.swift in Sources */, 525019DD2406430800EED91C /* ListProgressBarDataModel.swift in Sources */, C6FA7D5223C77A4A00A3614A /* UnOrderedList.swift in Sources */, 01509D8F2327EC6F00EF99AA /* MoleculeTableViewCell.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/TextFields/EntryField.swift b/MVMCoreUI/Atomic/Atoms/TextFields/EntryField.swift index 1fe0b1af..e54cb80f 100644 --- a/MVMCoreUI/Atomic/Atoms/TextFields/EntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/TextFields/EntryField.swift @@ -69,6 +69,7 @@ import UIKit get { return entryFieldContainer.showError } set (error) { self.feedback = error ? entryFieldModel?.errorMessage : entryFieldModel?.feedback + self.feedbackLabel.textColor = error ? entryFieldModel?.errorTextColor?.uiColor ?? .mvmBlack : .mvmBlack self.entryFieldContainer.showError = error self.entryFieldModel?.showError = error } @@ -215,11 +216,10 @@ import UIKit entryFieldContainer.refreshUI() } - /** - Method to override. - Intended to add the interactive content (i.e. textField) to the entryFieldContainer. - */ - @objc open func setupFieldContainerContent(_ container: UIView) { } + /// Intended to add the interactive content (i.e. textField) to the entryFieldContainer. + @objc open func setupFieldContainerContent(_ container: UIView) { + // To Be Overriden + } @objc open override func updateView(_ size: CGFloat) { super.updateView(size) @@ -242,6 +242,7 @@ import UIKit titleLabel.textColor = .mvmBlack feedbackLabel.font = Styler.Font.RegularMicro.getFont() feedbackLabel.textColor = .mvmBlack + entryFieldContainer.disableAllBorders = false feedbackLabel.text = nil entryFieldContainer.reset() } @@ -257,6 +258,7 @@ import UIKit title = model.title feedback = model.feedback isEnabled = model.enabled + entryFieldContainer.disableAllBorders = model.hideBorders if let isLocked = model.locked { self.isLocked = isLocked diff --git a/MVMCoreUI/Atomic/Atoms/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/TextFields/EntryFieldModel.swift index f7886848..1d6e9221 100644 --- a/MVMCoreUI/Atomic/Atoms/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/TextFields/EntryFieldModel.swift @@ -22,8 +22,10 @@ import Foundation public var title: String? public var feedback: String? public var errorMessage: String? + public var errorTextColor: Color? public var enabled: Bool = true public var showError: Bool? + public var hideBorders = false public var locked: Bool? public var selected: Bool? public var text: String? @@ -37,7 +39,7 @@ import Foundation } /// Temporary binding mechanism for the view to update on enable changes. - public var updateUI: (() -> ())? + public var updateUI: ActionBlock? //-------------------------------------------------- // MARK: - Keys @@ -50,9 +52,11 @@ import Foundation case enabled case feedback case errorMessage + case errorTextColor case locked case selected case showError + case hideBorders case text case fieldKey case groupName @@ -67,7 +71,12 @@ import Foundation } public func setValidity(_ valid: Bool, rule: RulesProtocol) { + if let fieldKey = fieldKey, + let ruleErrorMessage = rule.errorMessage?[fieldKey] { + self.errorMessage = ruleErrorMessage + } self.isValid = valid + } //-------------------------------------------------- @@ -89,10 +98,12 @@ import Foundation title = try typeContainer.decodeIfPresent(String.self, forKey: .title) feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback) errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) + errorTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .errorTextColor) enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true locked = try typeContainer.decodeIfPresent(Bool.self, forKey: .locked) selected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) text = try typeContainer.decodeIfPresent(String.self, forKey: .text) + hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) ?? false baseValue = text fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) @@ -111,8 +122,10 @@ import Foundation try container.encodeIfPresent(locked, forKey: .locked) try container.encodeIfPresent(showError, forKey: .showError) try container.encodeIfPresent(selected, forKey: .selected) + try container.encodeIfPresent(errorTextColor, forKey: .errorTextColor) try container.encodeIfPresent(errorMessage, forKey: .errorMessage) try container.encode(enabled, forKey: .enabled) + try container.encode(hideBorders, forKey: .hideBorders) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) } diff --git a/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryField.swift index 894fadd4..9d7c8927 100644 --- a/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryField.swift @@ -244,14 +244,21 @@ import UIKit self.isValid = isValid if previousValidity && !isValid { - showError = true - observingTextFieldDelegate?.isInvalid?(textfield: self) + shouldShowError(true) } else if (!previousValidity && isValid) { - showError = false - observingTextFieldDelegate?.isValid?(textfield: self) + shouldShowError(false) + } + } + + func shouldShowError(_ showError: Bool) { + self.showError = showError + if showError { + observingTextFieldDelegate?.isValid?(textfield: self) + entryFieldContainer.originalUI() + } else { + observingTextFieldDelegate?.isInvalid?(textfield: self) } } - /// Executes on UITextField.textDidBeginEditingNotification @objc func startEditing() { isSelected = true @@ -268,10 +275,16 @@ import UIKit /// Executes on UITextField.textDidEndEditingNotification @objc func endInputing() { resignFirstResponder() - if isValid { - showError = false - entryFieldContainer.bottomBar?.backgroundColor = UIColor.mvmBlack.cgColor + + // Don't show error till user starts typing. + guard text?.count ?? 0 != 0 else { + return } + + if let isValid = (model as? TextEntryFieldModel)?.isValid { + self.isValid = isValid + } + shouldShowError(!isValid) } @objc public func dismissFieldInput(_ sender: Any?) { diff --git a/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryFieldModel.swift index d47b9802..db5f3724 100644 --- a/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/TextFields/TextEntryFieldModel.swift @@ -31,6 +31,7 @@ public var placeholder: String? public var enabledTextColor: Color = Color(uiColor: .mvmBlack) public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3) + public var textAlignment: NSTextAlignment = .left public var type: EntryType? //-------------------------------------------------- @@ -39,6 +40,7 @@ private enum CodingKeys: String, CodingKey { case placeholder + case textAlignment case enabledTextColor case disabledTextColor case type @@ -51,6 +53,7 @@ required public init(from decoder: Decoder) throws { try super.init(from: decoder) let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + placeholder = try typeContainer.decodeIfPresent(String.self, forKey: .placeholder) type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) @@ -61,12 +64,17 @@ if let disabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledTextColor) { self.disabledTextColor = disabledTextColor } + + if let textAlignment = try typeContainer.decodeIfPresent(NSTextAlignment.self, forKey: .textAlignment) { + self.textAlignment = textAlignment + } } public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(placeholder, forKey: .placeholder) + try container.encodeIfPresent(textAlignment, forKey: .textAlignment) try container.encode(enabledTextColor, forKey: .enabledTextColor) try container.encode(disabledTextColor, forKey: .disabledTextColor) try container.encodeIfPresent(type, forKey: .type) diff --git a/MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryField.swift b/MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryField.swift new file mode 100644 index 00000000..546b5c30 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryField.swift @@ -0,0 +1,279 @@ +// +// TextViewEntryField.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 5/7/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + open private(set) var textView: TextView = { + let textView = TextView() + textView.setContentCompressionResistancePriority(.required, for: .vertical) + return textView + }() + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + /// Validate on each entry in the textView. Default: true + public var validateEachCharacter: Bool = true + + private var observingForChange: Bool = false + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + public var textViewEntryFieldModel: TextViewEntryFieldModel? { + return model as? TextViewEntryFieldModel + } + + public override var isEnabled: Bool { + get { return 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 = (enabled ? self.textViewEntryFieldModel?.enabledTextColor : self.textViewEntryFieldModel?.disabledTextColor)?.uiColor + } + } + } + } + + public override var showError: Bool { + get { return 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 { return textView.text } + set { + textView.text = newValue + textViewEntryFieldModel?.text = newValue + } + } + + /// Placeholder access for the textView. + public var placeholder: String? { + get { return textViewEntryFieldModel?.placeholder } + set { + textView.placeholder = newValue ?? "" + textViewEntryFieldModel?.placeholder = newValue + textView.setPlaceholderIfAvailable() + } + } + + //-------------------------------------------------- + // 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 + } + + //-------------------------------------------------- + // 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) + } + } + } + + /// If you're using a ViewController, you must set this to it + public weak var uiTextViewDelegate: UITextViewDelegate? { + get { return textView.delegate } + set { textView.delegate = newValue } + } + + @objc public 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) { + + container.addSubview(textView) + + 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) + + topConstraint?.isActive = true + leadingConstraint?.isActive = true + trailingConstraint?.isActive = true + bottomConstraint?.isActive = true + + heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0) + accessibilityElements = [titleLabel, textView, feedbackLabel] + } + + 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 func validateTextView() { + text = textView.text + if let isValid = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) { + self.isValid = isValid + } + } + + /// Executes on UITextView.textDidBeginEditingNotification + @objc func startEditing() { + isSelected = true + _ = textView.becomeFirstResponder() + } + + /// Executes on UITextView.textDidChangeNotification (each character entry) + @objc func valueChanged() { + guard validateEachCharacter else { return } + validateTextView() + } + + /// Executes on UITextView.textDidEndEditingNotification + @objc func endInputing() { + resignFirstResponder() + isSelected = false + showError = !isValid + } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + + guard let model = model as? TextViewEntryFieldModel else { return } + + if let height = model.height { + heightConstraint?.constant = height + heightConstraint?.isActive = true + } + + text = model.text + uiTextViewDelegate = delegateObject?.uiTextViewDelegate + observingTextViewDelegate = delegateObject?.observingTextFieldDelegate + + if let accessibilityText = model.accessibilityText { + accessibilityLabel = accessibilityText + } + + textView.isEditable = model.editable + textView.textAlignment = model.textAlignment + textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor + textView.font = model.fontStyle.getFont() + textView.placeholder = model.placeholder ?? "" + textView.placeholderFontStyle = model.placeholderFontStyle + textView.placeholderTextColor = model.placeholderTextColor.uiColor + textView.setPlaceholderIfAvailable() + + switch model.type { + case .secure, .password: + textView.isSecureTextEntry = true + + case .number: + textView.keyboardType = .numberPad + + case .email: + textView.keyboardType = .emailAddress + + default: break + } + + /// No point in configuring if the TextView is Read-only. + if textView.isEditable { + FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) + setupTextViewToolbar() + + if isSelected { + DispatchQueue.main.async { + _ = self.textView.becomeFirstResponder() + } + } + } + + if model.hideBorders { + adjustMarginConstraints(constant: 0) + } + + if !model.enabled { + isEnabled = false + } + } +} diff --git a/MVMCoreUI/BaseClasses/TextViewModel.swift b/MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryFieldModel.swift similarity index 75% rename from MVMCoreUI/BaseClasses/TextViewModel.swift rename to MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryFieldModel.swift index d9505275..ce1173ae 100644 --- a/MVMCoreUI/BaseClasses/TextViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/TextFields/TextViewEntryFieldModel.swift @@ -1,15 +1,15 @@ // -// TextViewModel.swift +// TextViewEntryFieldModel.swift // MVMCoreUI // -// Created by Kevin Christiano on 4/2/20. +// Created by Kevin Christiano on 5/7/20. // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation +import UIKit -open class TextViewModel: TextEntryFieldModel { +class TextViewEntryFieldModel: TextEntryFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -19,26 +19,21 @@ open class TextViewModel: TextEntryFieldModel { } public var accessibilityText: String? - public var fontStyle: Styler.Font = Styler.Font.RegularBodySmall - public var textAlignment: NSTextAlignment = .left + 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 showsPlaceholder: Bool = false - public var hideBorders: Bool = false public var editable: Bool = true - + public var showsPlaceholder: Bool = false + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { - case text case accessibilityText case fontStyle - case textAlignment case height - case hideBorders case placeholderFontStyle case placeholderTextColor case editable @@ -60,22 +55,14 @@ open class TextViewModel: TextEntryFieldModel { self.placeholderTextColor = placeholderTextColor } - if let textAlignment = try typeContainer.decodeIfPresent(NSTextAlignment.self, forKey: .textAlignment) { - self.textAlignment = textAlignment - } - - if let hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) { - self.hideBorders = hideBorders + 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 } - if let fontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .fontStyle) { - self.fontStyle = fontStyle - } - accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) } @@ -86,11 +73,8 @@ open class TextViewModel: TextEntryFieldModel { try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encodeIfPresent(height, forKey: .height) try container.encode(fontStyle, forKey: .fontStyle) - try container.encode(hideBorders, forKey: .hideBorders) - try container.encode(text, forKey: .text) + try container.encode(editable, forKey: .editable) try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle) try container.encode(placeholderTextColor, forKey: .placeholderTextColor) - try container.encode(textAlignment, forKey: .textAlignment) - try container.encode(editable, forKey: .editable) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Line.swift b/MVMCoreUI/Atomic/Atoms/Views/Line.swift index 62550503..42043c8f 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Line.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Line.swift @@ -32,16 +32,16 @@ import UIKit switch style { case .standard: updateLineConstraints(constant: 1) - backgroundColor = lineModel?.backgroundColor?.uiColor ?? .mfSilver() + backgroundColor = lineModel?.backgroundColor?.uiColor ?? .mvmCoolGray3 case .thin: updateLineConstraints(constant: 1) - backgroundColor = lineModel?.backgroundColor?.uiColor ?? .black + backgroundColor = lineModel?.backgroundColor?.uiColor ?? .mvmBlack case .medium: updateLineConstraints(constant: 2) - backgroundColor = lineModel?.backgroundColor?.uiColor ?? .black + backgroundColor = lineModel?.backgroundColor?.uiColor ?? .mvmBlack case .heavy: updateLineConstraints(constant: 4) - backgroundColor = lineModel?.backgroundColor?.uiColor ?? .black + backgroundColor = lineModel?.backgroundColor?.uiColor ?? .mvmBlack case .none: updateLineConstraints(constant: 0) } @@ -97,6 +97,7 @@ import UIKit } extension Line: MVMCoreUIViewConstrainingProtocol { + open func needsToBeConstrained() -> Bool { return true } diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index 31c4fac3..76c17c6a 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -52,7 +52,7 @@ import Foundation try? ModelRegistry.register(LabelAttributeActionModel.self) // TextView - MoleculeObjectMapping.shared()?.register(viewClass: TextView.self, viewModelClass: TextViewModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: TextViewEntryField.self, viewModelClass: TextViewEntryFieldModel.self) // Buttons MoleculeObjectMapping.shared()?.register(viewClass: PillButton.self, viewModelClass: ButtonModel.self) diff --git a/MVMCoreUI/BaseClasses/TextView.swift b/MVMCoreUI/BaseClasses/TextView.swift index 191806bb..2c609e02 100644 --- a/MVMCoreUI/BaseClasses/TextView.swift +++ b/MVMCoreUI/BaseClasses/TextView.swift @@ -9,7 +9,7 @@ import UIKit -@objc open class TextView: UITextView, UITextViewDelegate, MVMCoreViewProtocol { +@objc open class TextView: UITextView, MVMCoreViewProtocol, MoleculeViewProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -19,87 +19,18 @@ import UIKit private var initialSetupPerformed = false /// If true then text textView is currently displaying the stored placeholder text as there is not content to display. - public var isShowingPlaceholder: Bool = false { - didSet { textViewModel?.showsPlaceholder = isShowingPlaceholder } - } + public var isShowingPlaceholder: Bool = false /// Set to true to hide the blinking textField cursor. public var hideBlinkingCaret = false - public var textViewModel: TextViewModel? { - return model as? TextViewModel - } + public var placeholder = "" + public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge + public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro + public var placeholderTextColor: UIColor = .mvmCoolGray3 - //-------------------------------------------------- - // MARK: - Drawing Properties - //-------------------------------------------------- - - private(set) var fieldState: FieldState = .original { - didSet (oldState) { - // Will not update if new state is the same as old. - if fieldState != oldState { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.fieldState.setStateUI(for: self) - } - } - } - } - - /// Determines if the top, left, and right borders should be drawn. - private var hideBorders = false - - public var borderStrokeColor: UIColor = .mvmCoolGray3 - public var bottomStrokeColor: UIColor = .mvmBlack - private var borderPath: UIBezierPath = UIBezierPath() - private var bottomPath: UIBezierPath = UIBezierPath() - - //-------------------------------------------------- - // MARK: - Property Observers - //-------------------------------------------------- - - private var _isEnabled: Bool = true - private var _showError: Bool = false - private var _isSelected: Bool = false - - public var isEnabled: Bool { - get { return _isEnabled } - set (enabled) { - - _isEnabled = enabled - _isSelected = false - _showError = false - - fieldState = enabled ? .original : .disabled - } - } - - public var showError: Bool { - get { return _showError } - set (error) { - - _showError = error - _isEnabled = true - _isSelected = false - - fieldState = error ? .error : .original - } - } - - public var isSelected: Bool { - get { return _isSelected } - set (selected) { - - _isSelected = selected - _isEnabled = true - - if _showError { - fieldState = selected ? .selectedError : .error - } else { - fieldState = selected ? .selected : .original - } - } + public var isEnabled: Bool = true { + didSet { isUserInteractionEnabled = isEnabled } } //-------------------------------------------------- @@ -109,26 +40,6 @@ import UIKit /// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well. public weak var didDeleteDelegate: TextInputDidDeleteProtocol? - /// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well. - private weak var proprietorTextDelegate: UITextViewDelegate? - - /// If you're using a ViewController, you must set this to it. - public weak var uiTextViewDelegate: UITextViewDelegate? { - get { return delegate } - set { - delegate = self - proprietorTextDelegate = newValue - } - } - - var delegateObject: MVMCoreUIDelegateObject? - - //-------------------------------------------------- - // MARK: - Constraint - //-------------------------------------------------- - - public var heightConstraint: NSLayoutConstraint? - //-------------------------------------------------- // MARK: - Initialization //-------------------------------------------------- @@ -147,11 +58,6 @@ import UIKit initialSetup() } - convenience init(delegate: UITextViewDelegate) { - self.init(frame: .zero, textContainer: nil) - self.delegate = delegate - } - //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -166,172 +72,53 @@ import UIKit } open func updateView(_ size: CGFloat) { - - setNeedsDisplay() + font = (isShowingPlaceholder ? placeholderFontStyle : fontStyle).getFont() } /// Will be called only once. open func setupView() { translatesAutoresizingMaskIntoConstraints = false - initialConfiguration() + defaultConfiguration() } - public func initialConfiguration() { + public func defaultConfiguration() { + text = "" + placeholder = "" + textAlignment = .left insetsLayoutMarginsFromSafeArea = false showsVerticalScrollIndicator = false showsHorizontalScrollIndicator = false isSecureTextEntry = false - textContainerInset = UIEdgeInsets(top: Padding.Three, left: Padding.Three, bottom: Padding.Three, right: Padding.Three) backgroundColor = .mvmWhite clipsToBounds = true smartQuotesType = .no smartDashesType = .no smartInsertDeleteType = .no inputAccessoryView = nil - font = textViewModel?.fontStyle.getFont() + isAccessibilityElement = true + accessibilityTraits = .staticText + font = fontStyle.getFont() + keyboardType = .default isEditable = true - isOpaque = false } open func reset() { + fontStyle = Styler.Font.RegularBodyLarge + placeholderFontStyle = Styler.Font.RegularMicro + placeholderTextColor = .mvmCoolGray3 + textColor = .mvmBlack + isEnabled = true text = "" + isShowingPlaceholder = false inputAccessoryView?.removeFromSuperview() - inputAccessoryView = nil - initialConfiguration() - } - - open override func layoutSubviews() { - super.layoutSubviews() - - setNeedsDisplay() + defaultConfiguration() } //-------------------------------------------------- - // MARK: - Draw - //-------------------------------------------------- - - /// This handles the top, left, and right border lines. - open override func draw(_ rect: CGRect) { - super.draw(rect) - - borderPath.removeAllPoints() - bottomPath.removeAllPoints() - - if !hideBorders { - // Brings the other half of the line inside the view to prevent line cropping. - let origin = bounds.origin - let size = frame.size - let insetLean: CGFloat = 0.5 - borderPath.lineWidth = 1 - - // Drawing begins and ends from the bottom left. - borderPath.move(to: CGPoint(x: origin.x + insetLean, y: origin.y + size.height)) - borderPath.addLine(to: CGPoint(x: origin.x + insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + size.height)) - - borderStrokeColor.setStroke() - borderPath.stroke() - - let lineWidth: CGFloat = showError || isSelected ? 4 : 1 - bottomPath.lineWidth = lineWidth - bottomPath.move(to: CGPoint(x: origin.x + size.width, y: origin.y + size.height - (lineWidth / 2))) - bottomPath.addLine(to: CGPoint(x: origin.x, y: origin.y + size.height - (lineWidth / 2))) - - bottomStrokeColor.setStroke() - bottomPath.stroke() - } - } - - //-------------------------------------------------- - // MARK: - Draw States - //-------------------------------------------------- - - public enum FieldState { - case original - case error - case selectedError - case selected - case disabled - - public func setStateUI(for inputField: TextView) { - - switch self { - case .original: - inputField.originalUI() - - case .error: - inputField.errorUI() - - case .selectedError: - inputField.selectedErrorUI() - - case .selected: - inputField.selectedUI() - - case .disabled: - inputField.disabledUI() - } - - inputField.setNeedsDisplay() - } - } - - open func originalUI() { - - isEditable = textViewModel?.editable ?? true - isUserInteractionEnabled = true - hideBorders = textViewModel?.hideBorders ?? false - borderStrokeColor = .mvmCoolGray3 - bottomStrokeColor = .mvmBlack - textColor = isShowingPlaceholder ? textViewModel?.placeholderTextColor.uiColor : textViewModel?.enabledTextColor.uiColor - } - - open func errorUI() { - - isEditable = textViewModel?.editable ?? true - isUserInteractionEnabled = true - hideBorders = textViewModel?.hideBorders ?? false - borderStrokeColor = .mvmOrange - bottomStrokeColor = .mvmOrange - textColor = textViewModel?.enabledTextColor.uiColor - } - - open func selectedErrorUI() { - - isEditable = textViewModel?.editable ?? true - isUserInteractionEnabled = true - hideBorders = textViewModel?.hideBorders ?? false - borderStrokeColor = .mvmBlack - bottomStrokeColor = .mvmOrange - textColor = textViewModel?.enabledTextColor.uiColor - } - - open func selectedUI() { - - isEditable = textViewModel?.editable ?? true - isUserInteractionEnabled = true - hideBorders = textViewModel?.hideBorders ?? false - borderStrokeColor = .mvmBlack - bottomStrokeColor = .mvmBlack - textColor = textViewModel?.enabledTextColor.uiColor - } - - open func disabledUI() { - - isEditable = textViewModel?.editable ?? false - isUserInteractionEnabled = false - hideBorders = textViewModel?.hideBorders ?? false - borderStrokeColor = .mvmCoolGray3 - bottomStrokeColor = .mvmCoolGray3 - textColor = textViewModel?.disabledTextColor.uiColor - } - - //-------------------------------------------------- - // MARK: - Methods + // MARK: - TextInputDidDeleteProtocol //-------------------------------------------------- /// Alters the blinking caret line as per design standards. @@ -350,141 +137,49 @@ import UIKit didDeleteDelegate?.textInputDidDelete() } - public func setTextAppearance() { + //-------------------------------------------------- + // MARK: - Text / Placeholder + //-------------------------------------------------- + + open override func becomeFirstResponder() -> Bool { if isShowingPlaceholder { setTextContentTraits() } + return super.becomeFirstResponder() + } + + open override func resignFirstResponder() -> Bool { + + setPlaceholderIfAvailable() + return super.resignFirstResponder() } public func setPlaceholderIfAvailable() { - if let placeholder = textViewModel?.placeholder, !placeholder.isEmpty && text.isEmpty { + if !placeholder.isEmpty && text.isEmpty { setPlaceholderContentTraits() } } - public func setTextContentTraits() { + open func setTextContentTraits() { isShowingPlaceholder = false text = "" - font = textViewModel?.fontStyle.getFont() - textColor = textViewModel?.enabledTextColor.uiColor + font = fontStyle.getFont() + textColor = .mvmBlack } - public func setPlaceholderContentTraits() { + open func setPlaceholderContentTraits() { isShowingPlaceholder = true - textColor = textViewModel?.placeholderTextColor.uiColor - font = textViewModel?.placeholderFontStyle.getFont() - text = textViewModel?.placeholder + textColor = placeholderTextColor + font = placeholderFontStyle.getFont() + text = placeholder } - @objc func dismissFieldInput(_ sender: TextView) { + @objc open func dismissFieldInput(_ sender: TextView) { - resignFirstResponder() - } - - //-------------------------------------------------- - // MARK: - UITextViewDelegate - //-------------------------------------------------- - - @objc public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - - return proprietorTextDelegate?.textViewShouldBeginEditing?(textView) ?? true - } - - @objc public func textViewDidBeginEditing(_ textView: UITextView) { - - setTextAppearance() - isSelected = true - proprietorTextDelegate?.textViewDidBeginEditing?(textView) - } - - @objc public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - - return proprietorTextDelegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) ?? true - } - - @objc public func textViewDidChange(_ textView: UITextView) { - - textViewModel?.text = textView.text - proprietorTextDelegate?.textViewDidChange?(textView) - } - - @objc public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { - - return proprietorTextDelegate?.textViewShouldEndEditing?(textView) ?? true - } - - @objc public func textViewDidEndEditing(_ textView: UITextView) { - - setPlaceholderIfAvailable() - isSelected = false - proprietorTextDelegate?.textViewDidEndEditing?(textView) - } -} - -// MARK:- MoleculeViewProtocol -extension TextView: MoleculeViewProtocol { - - open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - self.model = model - self.delegateObject = delegateObject - - if let color = model.backgroundColor?.uiColor { - backgroundColor = color - } - - guard let model = model as? TextViewModel else { return } - - heightConstraint?.isActive = false - if let height = model.height { - heightConstraint = heightAnchor.constraint(equalToConstant: height) - heightConstraint?.isActive = true - } - - isEditable = model.editable - textAlignment = model.textAlignment - textColor = model.enabledTextColor.uiColor - hideBorders = model.hideBorders - text = model.text - uiTextViewDelegate = delegateObject?.uiTextViewDelegate - - if let accessibilityText = model.accessibilityText { - accessibilityLabel = accessibilityText - } - - switch model.type { - case .secure, .password: - isSecureTextEntry = true - case .number: - keyboardType = .numberPad - case .email: - keyboardType = .emailAddress - default: - break - } - - font = model.fontStyle.getFont() - setPlaceholderIfAvailable() - - if isEditable { - FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) - let observingDelegate = delegateObject?.uiTextViewDelegate ?? self - inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate, - action: #selector(dismissFieldInput)) - - if (model.selected ?? false) && !model.wasInitiallySelected { - model.wasInitiallySelected = true - DispatchQueue.main.async { - self.becomeFirstResponder() - } - } - } - - if !model.enabled { - isEnabled = false - } + _ = resignFirstResponder() } } diff --git a/MVMCoreUI/Containers/NavigationController.swift b/MVMCoreUI/Containers/NavigationController.swift index f278236c..05e5c406 100644 --- a/MVMCoreUI/Containers/NavigationController.swift +++ b/MVMCoreUI/Containers/NavigationController.swift @@ -58,7 +58,7 @@ import UIKit if navigationController == MVMCoreUISession.sharedGlobal()?.navigationController, navigationController.topViewController == viewController { // Update line. - MVMCoreUISession.sharedGlobal()?.navigationController?.separatorView?.setStyle(navigationItemModel.line?.type ?? .standard) + MVMCoreUISession.sharedGlobal()?.navigationController?.separatorView?.isHidden = navigationItemModel.line?.type ?? .standard == .none } if navigationController == MVMCoreUISplitViewController.main()?.navigationController, diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift index 1f50bcc7..aa80f5b0 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift @@ -15,6 +15,7 @@ public class RuleAllValueChangedModel: RulesProtocol { public static var identifier: String = "allValueChanged" public var type: String = RuleAllValueChangedModel.identifier + public var errorMessage: [String: String]? public var fields: [String] //-------------------------------------------------- diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift index 6ca905bf..7f153e83 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift @@ -17,6 +17,7 @@ public class RuleAnyRequiredModel: RulesProtocol { public static var identifier: String = "anyRequired" public var type: String = RuleRequiredModel.identifier public var fields: [String] + public var errorMessage: [String: String]? //-------------------------------------------------- // MARK: - Methods diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift index 450fdb2a..07cf451f 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift @@ -16,6 +16,7 @@ public class RuleAnyValueChangedModel: RulesProtocol { public static var identifier: String = "anyValueChanged" public var type: String = RuleAnyValueChangedModel.identifier + public var errorMessage: [String: String]? public var fields: [String] //-------------------------------------------------- diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift index fb7585f2..fa73ed51 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift @@ -17,6 +17,7 @@ public class RuleEqualsModel: RulesProtocol { public static var identifier: String = "equals" public var type: String = RuleEqualsModel.identifier public var fields: [String] + public var errorMessage: [String: String]? //-------------------------------------------------- // MARK: - Validation @@ -27,9 +28,9 @@ public class RuleEqualsModel: RulesProtocol { } public func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { - var valid = true - var compareValue: AnyHashable? - + var valid = true + var compareValue: AnyHashable? + for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } @@ -37,13 +38,16 @@ public class RuleEqualsModel: RulesProtocol { compareValue = formField.formFieldValue() continue } - + if compareValue != formField.formFieldValue() { valid = false + (formField as? FormRuleWatcherFieldProtocol)?.setValidity(valid, rule: self) break + } else { + (formField as? FormRuleWatcherFieldProtocol)?.setValidity(valid, rule: self) } } - - return valid + + return valid } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift index 68eea7e5..5f60a61b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift @@ -18,6 +18,7 @@ public class RuleRegexModel: RulesProtocol { public var type: String = RuleRegexModel.identifier public var fields: [String] public var regex: String + public var errorMessage: [String: String]? //-------------------------------------------------- // MARK: - Properties diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift index b34a24df..c9e7d9f7 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift @@ -16,6 +16,7 @@ public class RuleRequiredModel: RulesProtocol { public static var identifier: String = "allRequired" public var type: String = RuleRequiredModel.identifier + public var errorMessage: [String: String]? public var fields: [String] //-------------------------------------------------- diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift index 305b4c35..7392ea8f 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift @@ -16,7 +16,9 @@ public enum RulesCodingKey: String, CodingKey { public protocol RulesProtocol: ModelProtocol { // The type of rule var type: String { get } - + + var errorMessage: [String: String]? { get } + // The fields that this rule applies to. var fields: [String] { get set }