From 174d26edc424129bf5e1a5c7f20395c7aadc8f01 Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Fri, 3 Apr 2020 10:59:38 -0400 Subject: [PATCH] textview --- MVMCoreUI/BaseClasses/TextView.swift | 275 ++++++++++++++++++++++ MVMCoreUI/BaseClasses/TextViewModel.swift | 90 +++++++ 2 files changed, 365 insertions(+) create mode 100644 MVMCoreUI/BaseClasses/TextView.swift create mode 100644 MVMCoreUI/BaseClasses/TextViewModel.swift diff --git a/MVMCoreUI/BaseClasses/TextView.swift b/MVMCoreUI/BaseClasses/TextView.swift new file mode 100644 index 00000000..547de4a2 --- /dev/null +++ b/MVMCoreUI/BaseClasses/TextView.swift @@ -0,0 +1,275 @@ +// +// TextView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 4/1/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +@objc protocol MFTextViewDelegate { + // Dismisses the keyboard. + @objc optional func dismissFieldInput(_ sender: Any?) +} + + +@objc open class TextView: UITextView, UITextViewDelegate { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + open var model: MoleculeModelProtocol? + + private var initialSetupPerformed = false + + /// Set to true to hide the blinking textField cursor. + public var hideBlinkingCaret = false + + public var hideBorder = true + + public var borderPath: UIBezierPath? + + public var errorShowing = false + + weak var bottomLine: SeparatorView? + + public var placeholderFont: UIFont = MFStyler.fontB3()! { + didSet { + if text.isEmpty { + font = placeholderFont + } + } + } + + public var placeholder: String = "" { + didSet { + if text.isEmpty { + text = placeholder + } + } + } + + public var placeholderTextColor: UIColor = .mvmCoolGray3 + + public var displaysPlaceholder = false + + private(set) var textStore: String = "" + + public override var text: String! { + didSet { + if displaysPlaceholder && text.isEmpty { + text = placeholder + textColor = placeholderTextColor + } else { + textColor = .mvmBlack + } + } + } + + private(set) var textFont: UIFont = MFStyler.fontB2()! + + public override var font: UIFont? { + didSet { + if displaysPlaceholder && text.isEmpty { + font = placeholderFont + } else { + textColor = .mvmBlack + } + } + } + + //-------------------------------------------------- + // MARK: - Delegate + //-------------------------------------------------- + + /// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well. + public weak var didDeleteDelegate: TextFieldDidDeleteProtocol? + + /// If you're using a MFViewController, you must set this to it + public weak var uiTextViewDelegate: UITextViewDelegate? { + get { return delegate } + set { delegate = newValue } + } + + //-------------------------------------------------- + // 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(placeholderText: String, delegate: UITextViewDelegate?) { + self.init(frame: .zero, textContainer: nil) + self.delegate = delegate + displaysPlaceholder = true + self.placeholder = placeholderText + } + + public func initialSetup() { + + if !initialSetupPerformed { + tintColor = .black + initialSetupPerformed = true + setupView() + } + } + + 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() + } + + //-------------------------------------------------- + // MARK: - Drawing + //-------------------------------------------------- + + open override func draw(_ rect: CGRect) { + super.draw(rect) + + borderPath?.removeAllPoints() + + if !hideBorder { + layer.cornerRadius = 0 + borderPath = UIBezierPath() + let width = frame.origin.x + frame.size.width + let height = frame.origin.y + frame.size.height + + borderPath?.move(to: CGPoint(x: frame.origin.x, y: height)) + borderPath?.addLine(to: CGPoint(x: frame.origin.x, y: frame.origin.y)) + borderPath?.addLine(to: CGPoint(x: width, y: frame.origin.y)) + borderPath?.addLine(to: CGPoint(x: width, y: height)) + borderPath?.lineWidth = 1 + + var strokeColor: UIColor? + + if errorShowing { + strokeColor = .mvmOrangeAA + bottomLine?.backgroundColor = .mvmOrangeAA + } else { + strokeColor = .mvmCoolGray3 + bottomLine?.backgroundColor = .mvmBlack + } + strokeColor?.setStroke() + borderPath?.stroke() + } + } + + //-------------------------------------------------- + // MARK: - Observing Methods + //-------------------------------------------------- + + /// Executes on UITextView.textDidEndEditingNotification + @objc open func dismissFieldInput() { + resignFirstResponder() + } + + //#pragma Mark UITextView Delegate methods forwarding + @objc public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + + return delegate?.textViewShouldBeginEditing?(textView) ?? true + } + + @objc public func textViewDidBeginEditing(_ textView: UITextView) { + + delegate?.textViewDidBeginEditing?(textView) + } + + @objc public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + + return delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) ?? true + } + + @objc public func textViewDidChange(_ textView: UITextView) { + + delegate?.textViewDidChange?(textView) + } + + @objc public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + return delegate?.textViewShouldEndEditing?(textView) ?? true + } + + @objc public func textViewDidEndEditing(_ textView: UITextView) { + + delegate?.textViewDidEndEditing?(textView) + } +} + +/// MARK:- MVMCoreViewProtocol +extension TextView: MVMCoreViewProtocol { + + open func updateView(_ size: CGFloat) { } + + /// Will be called only once. + open func setupView() { + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + clipsToBounds = true + errorShowing = false + hideBorder = false + layer.cornerRadius = CGFloat(CornerRadiusLarge) + // Disable SmartQuotes + smartQuotesType = .no + smartDashesType = .no + smartInsertDeleteType = .no + font = textFont + MVMCoreUICommonViewsUtility.addDismissToolbar(to: self, delegate: delegate) + textContainerInset = UIEdgeInsets(top: PaddingTwo, left: PaddingTwo, bottom: PaddingTwo, right: PaddingOne) + } +} + +/// MARK:- MoleculeViewProtocol +extension TextView: MoleculeViewProtocol { + + open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + self.model = model + + if let color = model.backgroundColor?.uiColor { + backgroundColor = color + } + + guard let model = model as? TextViewModel else { return } + + if let height = model.height { + heightConstraint = heightAnchor.constraint(equalToConstant: height) + heightConstraint?.isActive = true + } + + text = model.text + placeholder = model.placeholder ?? "" + MVMCoreUICommonViewsUtility.addDismissToolbar(to: self, delegate: delegate) + } + + open func reset() { + backgroundColor = .clear + text = "" + } +} diff --git a/MVMCoreUI/BaseClasses/TextViewModel.swift b/MVMCoreUI/BaseClasses/TextViewModel.swift new file mode 100644 index 00000000..655b6933 --- /dev/null +++ b/MVMCoreUI/BaseClasses/TextViewModel.swift @@ -0,0 +1,90 @@ +// +// TextViewModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 4/2/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + + +open class TextViewModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String = "textView" + public var backgroundColor: Color? + public var text: String = "" + public var accessibilityText: String? + public var textColor: Color? + public var fontStyle: LabelModel.FontStyle? + public var textAlignment: NSTextAlignment? + public var height: CGFloat? + public var placeholder: String? + public var isEditable: Bool = true + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case moleculeName + case text + case accessibilityText + case textColor + case backgroundColor + case fontStyle + case fontName + case fontSize + case textAlignment + case attributes + case height + case placeholder + case isEditable + } + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(text: String) { + self.text = text + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + if let text = try typeContainer.decodeIfPresent(String.self, forKey: .text) { + self.text = text + } + placeholder = try typeContainer.decodeIfPresent(String.self, forKey: .text) + accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) + textColor = try typeContainer.decodeIfPresent(Color.self, forKey: .textColor) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + fontStyle = try typeContainer.decodeIfPresent(LabelModel.FontStyle.self, forKey: .fontStyle) + textAlignment = try typeContainer.decodeIfPresent(NSTextAlignment.self, forKey: .textAlignment) + height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) + + if let isEditable = try typeContainer.decodeIfPresent(Bool.self, forKey: .isEditable) { + self.isEditable = isEditable + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(moleculeName, forKey: .moleculeName) + try container.encode(text, forKey: .text) + try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) + try container.encodeIfPresent(textColor, forKey: .textColor) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(fontStyle, forKey: .fontStyle) + try container.encodeIfPresent(textAlignment, forKey: .textAlignment) + try container.encodeIfPresent(height, forKey: .height) + try container.encodeIfPresent(isEditable, forKey: .isEditable) + } +}