TextAreaRow.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. // TextAreaRow.swift
  2. // Eureka ( https://github.com/xmartlabs/Eureka )
  3. //
  4. // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com )
  5. //
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. import Foundation
  25. import UIKit
  26. // TODO: Temporary workaround for Xcode 10 beta
  27. #if swift(>=4.2)
  28. import UIKit.UIGeometry
  29. extension UIEdgeInsets {
  30. static let zero = UIEdgeInsets()
  31. }
  32. #endif
  33. public enum TextAreaHeight {
  34. case fixed(cellHeight: CGFloat)
  35. case dynamic(initialTextViewHeight: CGFloat)
  36. }
  37. public enum TextAreaMode {
  38. case normal
  39. case readOnly
  40. }
  41. protocol TextAreaConformance: FormatterConformance {
  42. var placeholder: String? { get set }
  43. var textAreaHeight: TextAreaHeight { get set }
  44. var titlePercentage: CGFloat? { get set}
  45. }
  46. /**
  47. * Protocol for cells that contain a UITextView
  48. */
  49. public protocol AreaCell: TextInputCell {
  50. var textView: UITextView! { get }
  51. }
  52. extension AreaCell {
  53. public var textInput: UITextInput {
  54. return textView
  55. }
  56. }
  57. open class _TextAreaCell<T> : Cell<T>, UITextViewDelegate, AreaCell where T: Equatable, T: InputTypeInitiable {
  58. @IBOutlet public weak var textView: UITextView!
  59. @IBOutlet public weak var placeholderLabel: UILabel?
  60. private var awakeFromNibCalled = false
  61. required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  62. super.init(style: style, reuseIdentifier: reuseIdentifier)
  63. let textView = UITextView()
  64. self.textView = textView
  65. textView.translatesAutoresizingMaskIntoConstraints = false
  66. textView.keyboardType = .default
  67. textView.font = .preferredFont(forTextStyle: .body)
  68. textView.textContainer.lineFragmentPadding = 0
  69. textView.textContainerInset = UIEdgeInsets.zero
  70. textView.backgroundColor = .clear
  71. contentView.addSubview(textView)
  72. let placeholderLabel = UILabel()
  73. self.placeholderLabel = placeholderLabel
  74. placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
  75. placeholderLabel.numberOfLines = 0
  76. if #available(iOS 13.0, *) {
  77. placeholderLabel.textColor = UIColor.tertiaryLabel
  78. } else {
  79. placeholderLabel.textColor = UIColor(white: 0, alpha: 0.22)
  80. }
  81. placeholderLabel.font = textView.font
  82. contentView.addSubview(placeholderLabel)
  83. }
  84. required public init?(coder aDecoder: NSCoder) {
  85. super.init(coder: aDecoder)
  86. }
  87. open override func awakeFromNib() {
  88. super.awakeFromNib()
  89. awakeFromNibCalled = true
  90. }
  91. open var dynamicConstraints = [NSLayoutConstraint]()
  92. open override func setup() {
  93. super.setup()
  94. let textAreaRow = row as! TextAreaConformance
  95. switch textAreaRow.textAreaHeight {
  96. case .dynamic(_):
  97. height = { UITableView.automaticDimension }
  98. textView.isScrollEnabled = false
  99. case .fixed(let cellHeight):
  100. height = { cellHeight }
  101. }
  102. textView.delegate = self
  103. selectionStyle = .none
  104. if !awakeFromNibCalled {
  105. imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil)
  106. }
  107. setNeedsUpdateConstraints()
  108. }
  109. deinit {
  110. textView?.delegate = nil
  111. if !awakeFromNibCalled {
  112. imageView?.removeObserver(self, forKeyPath: "image")
  113. }
  114. }
  115. open override func update() {
  116. super.update()
  117. textLabel?.text = nil
  118. detailTextLabel?.text = nil
  119. textView.isEditable = !row.isDisabled
  120. if #available(iOS 13.0, *) {
  121. textView.textColor = row.isDisabled ? .tertiaryLabel : .label
  122. } else {
  123. textView.textColor = row.isDisabled ? .gray : .black
  124. }
  125. textView.text = row.displayValueFor?(row.value)
  126. placeholderLabel?.text = (row as? TextAreaConformance)?.placeholder
  127. placeholderLabel?.isHidden = textView.text.count != 0
  128. }
  129. open override func cellCanBecomeFirstResponder() -> Bool {
  130. return !row.isDisabled && textView?.canBecomeFirstResponder == true
  131. }
  132. open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool {
  133. // workaround to solve https://github.com/xmartlabs/Eureka/issues/887 UIKit issue
  134. textView?.perform(#selector(UITextView.becomeFirstResponder), with: nil, afterDelay: 0.0)
  135. return true
  136. }
  137. open override func cellResignFirstResponder() -> Bool {
  138. return textView?.resignFirstResponder() ?? true
  139. }
  140. open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  141. let obj = object as AnyObject?
  142. if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], obj === imageView && keyPathValue == "image" &&
  143. (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue, !awakeFromNibCalled {
  144. setNeedsUpdateConstraints()
  145. updateConstraintsIfNeeded()
  146. }
  147. }
  148. //Mark: Helpers
  149. private func displayValue(useFormatter: Bool) -> String? {
  150. guard let v = row.value else { return nil }
  151. if let formatter = (row as? FormatterConformance)?.formatter, useFormatter {
  152. return textView?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v)
  153. }
  154. return String(describing: v)
  155. }
  156. // MARK: TextFieldDelegate
  157. open func textViewDidBeginEditing(_ textView: UITextView) {
  158. formViewController()?.beginEditing(of: self)
  159. formViewController()?.textInputDidBeginEditing(textView, cell: self)
  160. if let textAreaConformance = (row as? TextAreaConformance), let _ = textAreaConformance.formatter, textAreaConformance.useFormatterOnDidBeginEditing ?? textAreaConformance.useFormatterDuringInput {
  161. textView.text = self.displayValue(useFormatter: true)
  162. } else {
  163. textView.text = self.displayValue(useFormatter: false)
  164. }
  165. }
  166. open func textViewDidEndEditing(_ textView: UITextView) {
  167. formViewController()?.endEditing(of: self)
  168. formViewController()?.textInputDidEndEditing(textView, cell: self)
  169. textViewDidChange(textView)
  170. textView.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil)
  171. }
  172. open func textViewDidChange(_ textView: UITextView) {
  173. if let textAreaConformance = row as? TextAreaConformance, case .dynamic = textAreaConformance.textAreaHeight, let tableView = formViewController()?.tableView {
  174. let currentOffset = tableView.contentOffset
  175. UIView.performWithoutAnimation {
  176. tableView.beginUpdates()
  177. tableView.endUpdates()
  178. }
  179. tableView.setContentOffset(currentOffset, animated: false)
  180. }
  181. placeholderLabel?.isHidden = textView.text.count != 0
  182. guard let textValue = textView.text else {
  183. row.value = nil
  184. return
  185. }
  186. guard let formatterRow = row as? FormatterConformance, let formatter = formatterRow.formatter else {
  187. row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value)
  188. return
  189. }
  190. if formatterRow.useFormatterDuringInput {
  191. let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(UnsafeMutablePointer<T>.allocate(capacity: 1))
  192. let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
  193. if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
  194. row.value = value.pointee as? T
  195. guard var selStartPos = textView.selectedTextRange?.start else { return }
  196. let oldVal = textView.text
  197. textView.text = row.displayValueFor?(row.value)
  198. selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textView, oldValue: oldVal, newValue: textView.text) ?? selStartPos
  199. textView.selectedTextRange = textView.textRange(from: selStartPos, to: selStartPos)
  200. return
  201. }
  202. } else {
  203. let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(UnsafeMutablePointer<T>.allocate(capacity: 1))
  204. let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
  205. if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
  206. row.value = value.pointee as? T
  207. }
  208. }
  209. }
  210. open func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  211. return formViewController()?.textInput(textView, shouldChangeCharactersInRange: range, replacementString: text, cell: self) ?? true
  212. }
  213. open func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
  214. if let textAreaRow = self.row as? _TextAreaRow, textAreaRow.textAreaMode == .readOnly {
  215. return false
  216. }
  217. return formViewController()?.textInputShouldBeginEditing(textView, cell: self) ?? true
  218. }
  219. open func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
  220. return formViewController()?.textInputShouldEndEditing(textView, cell: self) ?? true
  221. }
  222. open override func updateConstraints() {
  223. customConstraints()
  224. super.updateConstraints()
  225. }
  226. open func customConstraints() {
  227. guard !awakeFromNibCalled else { return }
  228. contentView.removeConstraints(dynamicConstraints)
  229. dynamicConstraints = []
  230. var views: [String: AnyObject] = ["textView": textView, "label": placeholderLabel!]
  231. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[label]", options: [], metrics: nil, views: views))
  232. if let textAreaConformance = row as? TextAreaConformance, case .dynamic(let initialTextViewHeight) = textAreaConformance.textAreaHeight {
  233. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView(>=initialHeight@800)]-|", options: [], metrics: ["initialHeight": initialTextViewHeight], views: views))
  234. } else {
  235. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView]-|", options: [], metrics: nil, views: views))
  236. }
  237. if let imageView = imageView, let _ = imageView.image {
  238. views["imageView"] = imageView
  239. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textView]-|", options: [], metrics: nil, views: views))
  240. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[label]-|", options: [], metrics: nil, views: views))
  241. } else if let titlePercentage = (row as? TextAreaConformance)?.titlePercentage, titlePercentage > 0.0 {
  242. textView.textAlignment = .right
  243. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[textView]-|", options: [], metrics: nil, views: views)
  244. let sideSpaces = (layoutMargins.right + layoutMargins.left)
  245. dynamicConstraints.append(NSLayoutConstraint(item: textView!,
  246. attribute: .width,
  247. relatedBy: .equal,
  248. toItem: contentView,
  249. attribute: .width,
  250. multiplier: 1 - titlePercentage,
  251. constant: -sideSpaces))
  252. } else {
  253. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textView]-|", options: [], metrics: nil, views: views))
  254. dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", options: [], metrics: nil, views: views))
  255. }
  256. contentView.addConstraints(dynamicConstraints)
  257. }
  258. }
  259. open class TextAreaCell: _TextAreaCell<String>, CellType {
  260. required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  261. super.init(style: style, reuseIdentifier: reuseIdentifier)
  262. }
  263. required public init?(coder aDecoder: NSCoder) {
  264. super.init(coder: aDecoder)
  265. }
  266. }
  267. open class AreaRow<Cell: CellType>: FormatteableRow<Cell>, TextAreaConformance where Cell: BaseCell, Cell: AreaCell {
  268. open var placeholder: String?
  269. open var textAreaHeight = TextAreaHeight.fixed(cellHeight: 110)
  270. open var textAreaMode = TextAreaMode.normal
  271. /// The percentage of the cell that should be occupied by the remaining space to the left of the textArea. This is equivalent to the space occupied by a title in FieldRow, making the textArea aligned to fieldRows using the same titlePercentage. This behavior works only if the cell does not contain an image, due to its automatically set constraints in the cell.
  272. open var titlePercentage: CGFloat?
  273. public required init(tag: String?) {
  274. super.init(tag: tag)
  275. }
  276. }
  277. open class _TextAreaRow: AreaRow<TextAreaCell> {
  278. required public init(tag: String?) {
  279. super.init(tag: tag)
  280. }
  281. }
  282. /// A row with a UITextView where the user can enter large text.
  283. public final class TextAreaRow: _TextAreaRow, RowType {
  284. required public init(tag: String?) {
  285. super.init(tag: tag)
  286. }
  287. }