first cut of textEntry
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
0ab9ad37a8
commit
a4b550cf03
@ -7,123 +7,63 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate {
|
||||||
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
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//------------------------------------------------------
|
||||||
|
open var viewModel: TextViewEntryFieldModel!
|
||||||
|
open var delegateObject: MVMCoreUIDelegateObject?
|
||||||
|
open var additionalData: [AnyHashable : Any]?
|
||||||
|
|
||||||
private var observingForChange: Bool = false
|
// Form Validation
|
||||||
|
var fieldKey: String?
|
||||||
|
var fieldValue: JSONValue?
|
||||||
|
var groupName: String?
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Stored Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
public var isValid: Bool = true
|
||||||
|
|
||||||
|
private var isEditting: Bool = false {
|
||||||
|
didSet {
|
||||||
|
viewModel.selected = isEditting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Computed Properties
|
// 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.
|
/// The text of this textView.
|
||||||
open override var text: String? {
|
open override var text: String? {
|
||||||
get { textViewEntryFieldModel?.text }
|
didSet {
|
||||||
set {
|
viewModel?.text = text
|
||||||
textView.text = newValue
|
|
||||||
textViewEntryFieldModel?.text = newValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Placeholder access for the textView.
|
/// Placeholder access for the textView.
|
||||||
public var placeholder: String? {
|
public var placeholder: String? {
|
||||||
get { textViewEntryFieldModel?.placeholder }
|
get { viewModel?.placeholder }
|
||||||
set {
|
set {
|
||||||
textView.placeholder = newValue ?? ""
|
textView.placeholder = newValue ?? ""
|
||||||
textViewEntryFieldModel?.placeholder = newValue
|
viewModel?.placeholder = newValue
|
||||||
textView.setPlaceholderIfAvailable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
override var errorText: String? {
|
||||||
// MARK: - Constraint
|
get {
|
||||||
//--------------------------------------------------
|
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||||
|
}
|
||||||
public var heightConstraint: NSLayoutConstraint?
|
set {}
|
||||||
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
|
// MARK: - Delegate Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||||
public weak var observingTextViewDelegate: ObservingTextFieldDelegate? {
|
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
|
/// If you're using a ViewController, you must set this to it
|
||||||
public weak var uiTextViewDelegate: UITextViewDelegate? {
|
public weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||||
@ -136,102 +76,49 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
|||||||
uiTextViewDelegate = delegate
|
uiTextViewDelegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
open func setupTextViewToolbar() {
|
|
||||||
let observingDelegate = observingTextViewDelegate ?? self
|
|
||||||
textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
|
|
||||||
action: #selector(observingDelegate.dismissFieldInput))
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
override func setup() {
|
||||||
|
super.setup()
|
||||||
|
|
||||||
@objc open override func setupFieldContainerContent(_ container: UIView) {
|
publisher(for: .valueChanged)
|
||||||
|
.sink { [weak self] control in
|
||||||
|
guard let self else { return }
|
||||||
|
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
container.addSubview(textView)
|
textView
|
||||||
|
.publisher(for: .editingDidBegin)
|
||||||
|
.sink { [weak self] textView in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = true
|
||||||
|
|
||||||
topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three)
|
}.store(in: &subscribers)
|
||||||
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
|
textView
|
||||||
leadingConstraint?.isActive = true
|
.publisher(for: .editingDidEnd)
|
||||||
trailingConstraint?.isActive = true
|
.sink { [weak self] textView in
|
||||||
bottomConstraint?.isActive = true
|
guard let self else { return }
|
||||||
|
isEditting = false
|
||||||
|
if let valid = viewModel.isValid {
|
||||||
|
isValid = valid
|
||||||
|
}
|
||||||
|
showError = !isValid
|
||||||
|
|
||||||
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0)
|
}.store(in: &subscribers)
|
||||||
accessibilityElements = [textView]
|
|
||||||
}
|
|
||||||
|
|
||||||
open override func updateView(_ size: CGFloat) {
|
//String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "")
|
||||||
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
|
// MARK: - MoleculeViewProtocol
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open func updateView(_ size: CGFloat) {}
|
||||||
|
|
||||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
open func viewModelDidUpdate() {
|
||||||
super.set(with: model, delegateObject, additionalData)
|
|
||||||
|
|
||||||
guard let model = model as? TextViewEntryFieldModel else { return }
|
text = viewModel.text
|
||||||
|
|
||||||
if let height = model.height {
|
|
||||||
heightConstraint?.constant = height
|
|
||||||
heightConstraint?.isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
text = model.text
|
|
||||||
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
||||||
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
||||||
|
|
||||||
@ -239,17 +126,12 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
|||||||
accessibilityLabel = accessibilityText
|
accessibilityLabel = accessibilityText
|
||||||
}
|
}
|
||||||
|
|
||||||
textView.isEditable = model.editable
|
textView.isEditable = viewModel.editable
|
||||||
textView.textAlignment = model.textAlignment
|
textView.textAlignment = viewModel.textAlignment
|
||||||
textView.accessibilityIdentifier = model.accessibilityIdentifier
|
textView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||||
textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor
|
textView.placeholder = viewModel.placeholder ?? ""
|
||||||
textView.font = model.fontStyle.getFont()
|
|
||||||
textView.placeholder = model.placeholder ?? ""
|
|
||||||
textView.placeholderFontStyle = model.placeholderFontStyle
|
|
||||||
textView.placeholderTextColor = model.placeholderTextColor.uiColor
|
|
||||||
textView.setPlaceholderIfAvailable()
|
|
||||||
|
|
||||||
switch model.type {
|
switch viewModel.type {
|
||||||
case .secure, .password:
|
case .secure, .password:
|
||||||
textView.isSecureTextEntry = true
|
textView.isSecureTextEntry = true
|
||||||
|
|
||||||
@ -266,40 +148,60 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
|||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||||
|
|
||||||
|
viewModel.wasInitiallySelected = true
|
||||||
|
isEditting = true
|
||||||
|
}
|
||||||
|
|
||||||
/// No point in configuring if the TextView is Read-only.
|
/// No point in configuring if the TextView is Read-only.
|
||||||
if textView.isEditable {
|
if textView.isEditable {
|
||||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
setupTextViewToolbar()
|
|
||||||
|
|
||||||
if isSelected {
|
if isEditting {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
_ = self.textView.becomeFirstResponder()
|
_ = self.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.hideBorders {
|
viewModel.updateUI = {
|
||||||
adjustMarginConstraints(constant: 0)
|
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 previousValidity && !isValid {
|
||||||
|
showError = true
|
||||||
if let titleText = model.accessibilityText ?? model.title {
|
} else if (!previousValidity && isValid) {
|
||||||
message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
|
showError = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let feedback = model.feedback {
|
|
||||||
message += ", " + feedback
|
|
||||||
}
|
|
||||||
|
|
||||||
if let errorMessage = errorLabel.text {
|
|
||||||
message += ", " + errorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
textView.accessibilityLabel = message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,9 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
public override class var identifier: String { "textView" }
|
public override class var identifier: String { "textView" }
|
||||||
|
|
||||||
public var accessibilityText: String?
|
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 editable: Bool = true
|
||||||
public var showsPlaceholder: Bool = false
|
public var showsPlaceholder: Bool = false
|
||||||
|
public var tooltip: TooltipModel?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Keys
|
// MARK: - Keys
|
||||||
@ -30,11 +27,8 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case accessibilityText
|
case accessibilityText
|
||||||
case fontStyle
|
|
||||||
case height
|
|
||||||
case placeholderFontStyle
|
|
||||||
case placeholderTextColor
|
|
||||||
case editable
|
case editable
|
||||||
|
case tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -45,34 +39,19 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
try super.init(from: decoder)
|
try super.init(from: decoder)
|
||||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
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) {
|
if let editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) {
|
||||||
self.editable = editable
|
self.editable = editable
|
||||||
}
|
}
|
||||||
|
|
||||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
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 {
|
public override func encode(to encoder: Encoder) throws {
|
||||||
try super.encode(to: encoder)
|
try super.encode(to: encoder)
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
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(editable, forKey: .editable)
|
||||||
try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle)
|
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||||
try container.encode(placeholderTextColor, forKey: .placeholderTextColor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user