500 lines
15 KiB
Swift
500 lines
15 KiB
Swift
//
|
|
// TextView.swift
|
|
// MVMCoreUI
|
|
//
|
|
// Created by Kevin Christiano on 4/1/20.
|
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
|
|
@objc open class TextView: UITextView, UITextViewDelegate, MVMCoreViewProtocol {
|
|
//--------------------------------------------------
|
|
// MARK: - Properties
|
|
//--------------------------------------------------
|
|
|
|
open var model: MoleculeModelProtocol?
|
|
|
|
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 = true
|
|
|
|
/// Set to true to hide the blinking textField cursor.
|
|
public var hideBlinkingCaret = false
|
|
|
|
public var textViewModel: TextViewModel? {
|
|
return model as? TextViewModel
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Drawing Properties
|
|
//--------------------------------------------------
|
|
|
|
/// Total control over the drawn top, bottom, left and right borders.
|
|
public var disableAllBorders = false
|
|
|
|
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 _isLocked: Bool = false
|
|
private var _isSelected: Bool = false
|
|
|
|
public var isEnabled: Bool {
|
|
get { return _isEnabled }
|
|
set (enabled) {
|
|
|
|
_isEnabled = enabled
|
|
_isLocked = false
|
|
_isSelected = false
|
|
_showError = false
|
|
|
|
fieldState = enabled ? .original : .disabled
|
|
}
|
|
}
|
|
|
|
public var showError: Bool {
|
|
get { return _showError }
|
|
set (error) {
|
|
|
|
_showError = error
|
|
_isEnabled = true
|
|
_isLocked = false
|
|
_isSelected = false
|
|
|
|
fieldState = error ? .error : .original
|
|
}
|
|
}
|
|
|
|
public var isLocked: Bool {
|
|
get { return _isLocked }
|
|
set (locked) {
|
|
|
|
_isLocked = locked
|
|
_isEnabled = true
|
|
_isSelected = false
|
|
_showError = false
|
|
|
|
fieldState = locked ? .locked : .original
|
|
}
|
|
}
|
|
|
|
public var isSelected: Bool {
|
|
get { return _isSelected }
|
|
set (selected) {
|
|
|
|
_isSelected = selected
|
|
_isLocked = false
|
|
_isEnabled = true
|
|
|
|
if _showError {
|
|
fieldState = selected ? .selectedError : .error
|
|
} else {
|
|
fieldState = selected ? .selected : .original
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Delegate
|
|
//--------------------------------------------------
|
|
|
|
/// 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
|
|
//--------------------------------------------------
|
|
|
|
public override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
super.init(frame: .zero, textContainer: nil)
|
|
initialSetup()
|
|
}
|
|
|
|
public convenience init() {
|
|
self.init(frame: .zero, textContainer: nil)
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
initialSetup()
|
|
}
|
|
|
|
convenience init(delegate: UITextViewDelegate) {
|
|
self.init(frame: .zero, textContainer: nil)
|
|
self.delegate = delegate
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Lifecycle
|
|
//--------------------------------------------------
|
|
|
|
public func initialSetup() {
|
|
|
|
if !initialSetupPerformed {
|
|
tintColor = .mvmBlack
|
|
initialSetupPerformed = true
|
|
setupView()
|
|
}
|
|
}
|
|
|
|
open func updateView(_ size: CGFloat) {
|
|
|
|
refreshUI()
|
|
}
|
|
|
|
/// Will be called only once.
|
|
open func setupView() {
|
|
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
insetsLayoutMarginsFromSafeArea = false
|
|
showsVerticalScrollIndicator = false
|
|
showsHorizontalScrollIndicator = false
|
|
contentInset = UIEdgeInsets(top: Padding.Three, left: Padding.Three, bottom: Padding.Three, right: Padding.Three)
|
|
backgroundColor = .mvmWhite
|
|
clipsToBounds = true
|
|
smartQuotesType = .no
|
|
smartDashesType = .no
|
|
smartInsertDeleteType = .no
|
|
font = textViewModel?.fontStyle.getFont()
|
|
isEditable = true
|
|
isOpaque = false
|
|
}
|
|
|
|
open func reset() {
|
|
|
|
backgroundColor = .mvmWhite
|
|
text = ""
|
|
inputAccessoryView?.removeFromSuperview()
|
|
contentInset = UIEdgeInsets(top: Padding.Three, left: Padding.Three, bottom: Padding.Three, right: Padding.Three)
|
|
}
|
|
|
|
open override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
refreshUI(bottomBarSize: showError ? 4 : 1)
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// 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 !disableAllBorders && !hideBorders {
|
|
// Brings the other half of the line inside the view to prevent cropping.
|
|
let origin = bounds.origin
|
|
let size = frame.size
|
|
let insetLean: CGFloat = 0.5
|
|
borderPath.lineWidth = 1
|
|
|
|
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()
|
|
|
|
bottomPath.lineWidth = 4
|
|
bottomPath.move(to: CGPoint(x: origin.x + size.width, y: origin.y + size.height - 2))
|
|
bottomPath.addLine(to: CGPoint(x: origin.x, y: origin.y + size.height - 2))
|
|
|
|
bottomStrokeColor.setStroke()
|
|
bottomPath.stroke()
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Draw States
|
|
//--------------------------------------------------
|
|
|
|
public enum FieldState {
|
|
case original
|
|
case error
|
|
case selectedError
|
|
case selected
|
|
case locked
|
|
case disabled
|
|
|
|
public func setStateUI(for formField: TextView) {
|
|
|
|
switch self {
|
|
case .original:
|
|
formField.originalUI()
|
|
|
|
case .error:
|
|
formField.errorUI()
|
|
|
|
case .selectedError:
|
|
formField.selectedErrorUI()
|
|
|
|
case .selected:
|
|
formField.selectedUI()
|
|
|
|
case .locked:
|
|
formField.lockedUI()
|
|
|
|
case .disabled:
|
|
formField.disabledUI()
|
|
}
|
|
}
|
|
}
|
|
|
|
open func originalUI() {
|
|
|
|
isEditable = true
|
|
hideBorders = false
|
|
borderStrokeColor = .mvmCoolGray3
|
|
bottomStrokeColor = .mvmBlack
|
|
refreshUI(bottomBarSize: 1)
|
|
}
|
|
|
|
open func errorUI() {
|
|
|
|
isEditable = true
|
|
hideBorders = false
|
|
borderStrokeColor = .mvmOrange
|
|
bottomStrokeColor = .mvmOrange
|
|
refreshUI(bottomBarSize: 4)
|
|
}
|
|
|
|
open func selectedErrorUI() {
|
|
|
|
isEditable = true
|
|
hideBorders = false
|
|
borderStrokeColor = .mvmBlack
|
|
bottomStrokeColor = .mvmOrange
|
|
refreshUI(bottomBarSize: 4)
|
|
}
|
|
|
|
open func selectedUI() {
|
|
|
|
isEditable = true
|
|
hideBorders = false
|
|
borderStrokeColor = .mvmBlack
|
|
bottomStrokeColor = .mvmBlack
|
|
refreshUI(bottomBarSize: 1)
|
|
}
|
|
|
|
open func lockedUI() {
|
|
|
|
isEditable = false
|
|
hideBorders = true
|
|
borderStrokeColor = .clear
|
|
bottomStrokeColor = .clear
|
|
refreshUI(bottomBarSize: 1)
|
|
}
|
|
|
|
open func disabledUI() {
|
|
|
|
isEditable = false
|
|
hideBorders = false
|
|
borderStrokeColor = .mvmCoolGray3
|
|
bottomStrokeColor = .mvmCoolGray3
|
|
refreshUI(bottomBarSize: 1)
|
|
}
|
|
|
|
open func refreshUI(bottomBarSize: CGFloat? = nil, updateMoleculeLayout: Bool = false) {
|
|
|
|
if !disableAllBorders {
|
|
// let size: CGFloat = bottomBarSize ?? (showError ? 4 : 1)
|
|
// var heightChanged = false
|
|
|
|
// if let bottomHeight = bottomBar?.bounds.height {
|
|
// heightChanged = size != bottomHeight
|
|
// }
|
|
|
|
if updateMoleculeLayout {//|| heightChanged {
|
|
delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self)
|
|
}
|
|
|
|
setNeedsDisplay()
|
|
layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Methods
|
|
//--------------------------------------------------
|
|
|
|
/// Alters the blinking caret line as per design standards.
|
|
open override func caretRect(for position: UITextPosition) -> CGRect {
|
|
|
|
if hideBlinkingCaret {
|
|
return .zero
|
|
}
|
|
|
|
let caretRect = super.caretRect(for: position)
|
|
return CGRect(origin: caretRect.origin, size: CGSize(width: 1, height: caretRect.height))
|
|
}
|
|
|
|
open override func deleteBackward() {
|
|
super.deleteBackward()
|
|
didDeleteDelegate?.textFieldDidDelete()
|
|
}
|
|
|
|
public func setTextAppearance() {
|
|
|
|
if isShowingPlaceholder {
|
|
setTextContentTraits()
|
|
}
|
|
}
|
|
|
|
public func setPlaceholderIfAvailable() {
|
|
|
|
if let placeholder = textViewModel?.placeholder, !placeholder.isEmpty && text.isEmpty {
|
|
setPlaceholderContentTraits()
|
|
}
|
|
}
|
|
|
|
public func setTextContentTraits() {
|
|
|
|
isShowingPlaceholder = false
|
|
text = ""
|
|
font = textViewModel?.fontStyle.getFont()
|
|
textColor = textViewModel?.textColor.uiColor
|
|
}
|
|
|
|
public func setPlaceholderContentTraits() {
|
|
|
|
isShowingPlaceholder = true
|
|
textColor = textViewModel?.placeholderTextColor.uiColor
|
|
font = textViewModel?.placeholderFontStyle.getFont()
|
|
text = textViewModel?.placeholder
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - UITextViewDelegate
|
|
//--------------------------------------------------
|
|
|
|
@objc public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
|
|
|
|
return proprietorTextDelegate?.textViewShouldBeginEditing?(textView) ?? true
|
|
}
|
|
|
|
@objc public func textViewDidBeginEditing(_ textView: UITextView) {
|
|
|
|
setTextAppearance()
|
|
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()
|
|
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.isEditable
|
|
textAlignment = model.textAlignment
|
|
textColor = model.textColor.uiColor
|
|
text = model.text
|
|
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
|
isShowingPlaceholder = model.text!.isEmpty
|
|
|
|
if let accessibilityText = model.accessibilityText {
|
|
accessibilityLabel = accessibilityText
|
|
}
|
|
|
|
font = model.fontStyle.getFont()
|
|
setPlaceholderIfAvailable()
|
|
|
|
if isEditable {
|
|
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
|
MVMCoreUICommonViewsUtility.addDismissToolbar(to: self, delegate: delegateObject?.uiTextViewDelegate)
|
|
|
|
if model.selected ?? false {
|
|
isSelected = true
|
|
model.wasInitiallySelected = true
|
|
becomeFirstResponder()
|
|
}
|
|
}
|
|
}
|
|
}
|