Merge branch 'feature/atomic-vds-textArea' into 'develop'

VDS Brand 3.0 Text Area

### Summary
VDS Brand 3.0 Text Area for IOS

### JIRA Ticket
https://onejira.verizon.com/browse/ONEAPP-6682

Co-authored-by: Matt Bruce <matt.bruce@verizon.com>

See merge request https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui/-/merge_requests/1155
This commit is contained in:
Pfeil, Scott Robert 2024-07-30 18:51:43 +00:00
commit bf491e2341
5 changed files with 185 additions and 267 deletions

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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<ValueType>: VDSRuleBase {
// MARK: - Properties
//--------------------------------------------------
public var rule: AnyRule<ValueType>
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
@ -34,8 +37,8 @@ public class RuleVDSModel<ValueType>: VDSRuleBase {
public init(field: String, rule: AnyRule<ValueType>) {
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<ValueType>: VDSRuleBase {
return valid
}
}