FieldRow.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. // FieldRow.swift
  2. // Eureka ( https://github.com/xmartlabs/Eureka )
  3. //
  4. // Copyright (c) 2016 Xmartlabs ( 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. public protocol InputTypeInitiable {
  27. init?(string stringValue: String)
  28. }
  29. public protocol FieldRowConformance : FormatterConformance {
  30. var titlePercentage : CGFloat? { get set }
  31. var placeholder : String? { get set }
  32. var placeholderColor : UIColor? { get set }
  33. }
  34. extension Int: InputTypeInitiable {
  35. public init?(string stringValue: String) {
  36. self.init(stringValue, radix: 10)
  37. }
  38. }
  39. extension Float: InputTypeInitiable {
  40. public init?(string stringValue: String) {
  41. self.init(stringValue)
  42. }
  43. }
  44. extension String: InputTypeInitiable {
  45. public init?(string stringValue: String) {
  46. self.init(stringValue)
  47. }
  48. }
  49. extension URL: InputTypeInitiable {}
  50. extension Double: InputTypeInitiable {
  51. public init?(string stringValue: String) {
  52. self.init(stringValue)
  53. }
  54. }
  55. open class FormatteableRow<Cell: CellType>: Row<Cell>, FormatterConformance where Cell: BaseCell, Cell: TextInputCell {
  56. /// A formatter to be used to format the user's input
  57. open var formatter: Formatter?
  58. /// If the formatter should be used while the user is editing the text.
  59. open var useFormatterDuringInput = false
  60. open var useFormatterOnDidBeginEditing: Bool?
  61. public required init(tag: String?) {
  62. super.init(tag: tag)
  63. displayValueFor = { [unowned self] value in
  64. guard let v = value else { return nil }
  65. guard let formatter = self.formatter else { return String(describing: v) }
  66. if (self.cell.textInput as? UIView)?.isFirstResponder == true {
  67. return self.useFormatterDuringInput ? formatter.editingString(for: v) : String(describing: v)
  68. }
  69. return formatter.string(for: v)
  70. }
  71. }
  72. }
  73. open class FieldRow<Cell: CellType>: FormatteableRow<Cell>, FieldRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: TextFieldCell {
  74. /// Configuration for the keyboardReturnType of this row
  75. open var keyboardReturnType: KeyboardReturnTypeConfiguration?
  76. /// The percentage of the cell that should be occupied by the textField
  77. @available (*, deprecated, message: "Use titlePercentage instead")
  78. open var textFieldPercentage : CGFloat? {
  79. get {
  80. return titlePercentage.map { 1 - $0 }
  81. }
  82. set {
  83. titlePercentage = newValue.map { 1 - $0 }
  84. }
  85. }
  86. /// The percentage of the cell that should be occupied by the title (i.e. the titleLabel and optional imageView combined)
  87. open var titlePercentage: CGFloat?
  88. /// The placeholder for the textField
  89. open var placeholder: String?
  90. /// The textColor for the textField's placeholder
  91. open var placeholderColor: UIColor?
  92. public required init(tag: String?) {
  93. super.init(tag: tag)
  94. }
  95. }
  96. /**
  97. * Protocol for cells that contain a UITextField
  98. */
  99. public protocol TextInputCell {
  100. var textInput: UITextInput { get }
  101. }
  102. public protocol TextFieldCell: TextInputCell {
  103. var textField: UITextField! { get }
  104. }
  105. extension TextFieldCell {
  106. public var textInput: UITextInput {
  107. return textField
  108. }
  109. }
  110. open class _FieldCell<T> : Cell<T>, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable {
  111. @IBOutlet public weak var textField: UITextField!
  112. @IBOutlet public weak var titleLabel: UILabel?
  113. fileprivate var observingTitleText = false
  114. private var awakeFromNibCalled = false
  115. open var dynamicConstraints = [NSLayoutConstraint]()
  116. private var calculatedTitlePercentage: CGFloat = 0.7
  117. public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  118. let textField = UITextField()
  119. self.textField = textField
  120. textField.translatesAutoresizingMaskIntoConstraints = false
  121. super.init(style: style, reuseIdentifier: reuseIdentifier)
  122. setupTitleLabel()
  123. contentView.addSubview(titleLabel!)
  124. contentView.addSubview(textField)
  125. NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in
  126. guard let me = self else { return }
  127. guard me.observingTitleText else { return }
  128. me.titleLabel?.removeObserver(me, forKeyPath: "text")
  129. me.observingTitleText = false
  130. }
  131. NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
  132. guard let me = self else { return }
  133. guard !me.observingTitleText else { return }
  134. me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil)
  135. me.observingTitleText = true
  136. }
  137. NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in
  138. self?.setupTitleLabel()
  139. self?.setNeedsUpdateConstraints()
  140. }
  141. }
  142. required public init?(coder aDecoder: NSCoder) {
  143. super.init(coder: aDecoder)
  144. }
  145. open override func awakeFromNib() {
  146. super.awakeFromNib()
  147. awakeFromNibCalled = true
  148. }
  149. deinit {
  150. textField?.delegate = nil
  151. textField?.removeTarget(self, action: nil, for: .allEvents)
  152. guard !awakeFromNibCalled else { return }
  153. if observingTitleText {
  154. titleLabel?.removeObserver(self, forKeyPath: "text")
  155. }
  156. imageView?.removeObserver(self, forKeyPath: "image")
  157. NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
  158. NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
  159. NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil)
  160. }
  161. open override func setup() {
  162. super.setup()
  163. selectionStyle = .none
  164. if !awakeFromNibCalled {
  165. titleLabel?.addObserver(self, forKeyPath: "text", options: [.new, .old], context: nil)
  166. observingTitleText = true
  167. imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil)
  168. }
  169. textField.addTarget(self, action: #selector(_FieldCell.textFieldDidChange(_:)), for: .editingChanged)
  170. if let titleLabel = titleLabel {
  171. // Make sure the title takes over most of the empty space so that the text field starts editing at the back.
  172. let priority = UILayoutPriority(rawValue: titleLabel.contentHuggingPriority(for: .horizontal).rawValue + 1)
  173. textField.setContentHuggingPriority(priority, for: .horizontal)
  174. }
  175. }
  176. open override func update() {
  177. super.update()
  178. detailTextLabel?.text = nil
  179. if !awakeFromNibCalled {
  180. if let title = row.title {
  181. switch row.cellStyle {
  182. case .subtitle:
  183. textField.textAlignment = .left
  184. textField.clearButtonMode = .whileEditing
  185. default:
  186. textField.textAlignment = title.isEmpty ? .left : .right
  187. textField.clearButtonMode = title.isEmpty ? .whileEditing : .never
  188. }
  189. } else {
  190. textField.textAlignment = .left
  191. textField.clearButtonMode = .whileEditing
  192. }
  193. } else {
  194. textLabel?.text = nil
  195. titleLabel?.text = row.title
  196. if #available(iOS 13.0, *) {
  197. titleLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label
  198. } else {
  199. titleLabel?.textColor = row.isDisabled ? .gray : .black
  200. }
  201. }
  202. textField.delegate = self
  203. textField.text = row.displayValueFor?(row.value)
  204. textField.isEnabled = !row.isDisabled
  205. if #available(iOS 13.0, *) {
  206. textField.textColor = row.isDisabled ? .tertiaryLabel : .label
  207. } else {
  208. textField.textColor = row.isDisabled ? .gray : .black
  209. }
  210. textField.font = .preferredFont(forTextStyle: .body)
  211. if let placeholder = (row as? FieldRowConformance)?.placeholder {
  212. if let color = (row as? FieldRowConformance)?.placeholderColor {
  213. textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color])
  214. } else {
  215. textField.placeholder = (row as? FieldRowConformance)?.placeholder
  216. }
  217. }
  218. if row.isHighlighted {
  219. titleLabel?.textColor = tintColor
  220. }
  221. }
  222. open override func cellCanBecomeFirstResponder() -> Bool {
  223. return !row.isDisabled && textField?.canBecomeFirstResponder == true
  224. }
  225. open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool {
  226. return textField.becomeFirstResponder()
  227. }
  228. open override func cellResignFirstResponder() -> Bool {
  229. return textField.resignFirstResponder()
  230. }
  231. open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  232. let obj = object as AnyObject?
  233. if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey],
  234. ((obj === titleLabel && keyPathValue == "text") || (obj === imageView && keyPathValue == "image")) &&
  235. (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue {
  236. setNeedsUpdateConstraints()
  237. updateConstraintsIfNeeded()
  238. }
  239. }
  240. // MARK: Helpers
  241. open func customConstraints() {
  242. guard !awakeFromNibCalled else { return }
  243. contentView.removeConstraints(dynamicConstraints)
  244. dynamicConstraints = []
  245. switch row.cellStyle {
  246. case .subtitle:
  247. var views: [String: AnyObject] = ["textField": textField]
  248. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  249. views["titleLabel"] = titleLabel
  250. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-3-[textField]-|",
  251. options: .alignAllLeading, metrics: nil, views: views)
  252. titleLabel.setContentHuggingPriority(
  253. UILayoutPriority(textField.contentHuggingPriority(for: .vertical).rawValue + 1), for: .vertical)
  254. dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: textField, attribute: .centerX, multiplier: 1, constant: 0))
  255. } else {
  256. dynamicConstraints.append(NSLayoutConstraint(item: textField!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0))
  257. }
  258. if let imageView = imageView, let _ = imageView.image {
  259. views["imageView"] = imageView
  260. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  261. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-|", options: [], metrics: nil, views: views)
  262. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
  263. } else {
  264. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
  265. }
  266. } else {
  267. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  268. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-|", options: [], metrics: nil, views: views)
  269. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: [], metrics: nil, views: views)
  270. } else {
  271. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views)
  272. }
  273. }
  274. default:
  275. var views: [String: AnyObject] = ["textField": textField]
  276. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textField]-|", options: .alignAllLastBaseline, metrics: nil, views: views)
  277. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  278. views["titleLabel"] = titleLabel
  279. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-|", options: .alignAllLastBaseline, metrics: nil, views: views)
  280. dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: textField, attribute: .centerY, multiplier: 1, constant: 0))
  281. }
  282. if let imageView = imageView, let _ = imageView.image {
  283. views["imageView"] = imageView
  284. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  285. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views)
  286. dynamicConstraints.append(NSLayoutConstraint(item: titleLabel,
  287. attribute: .width,
  288. relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual,
  289. toItem: contentView,
  290. attribute: .width,
  291. multiplier: calculatedTitlePercentage,
  292. constant: 0.0))
  293. } else {
  294. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
  295. }
  296. } else {
  297. if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
  298. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views)
  299. dynamicConstraints.append(NSLayoutConstraint(item: titleLabel,
  300. attribute: .width,
  301. relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual,
  302. toItem: contentView,
  303. attribute: .width,
  304. multiplier: calculatedTitlePercentage,
  305. constant: 0.0))
  306. } else {
  307. dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views)
  308. }
  309. }
  310. }
  311. contentView.addConstraints(dynamicConstraints)
  312. }
  313. open override func updateConstraints() {
  314. customConstraints()
  315. super.updateConstraints()
  316. }
  317. @objc open func textFieldDidChange(_ textField: UITextField) {
  318. guard textField.markedTextRange == nil else { return }
  319. guard let textValue = textField.text else {
  320. row.value = nil
  321. return
  322. }
  323. guard let fieldRow = row as? FieldRowConformance, let formatter = fieldRow.formatter else {
  324. row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value)
  325. return
  326. }
  327. if fieldRow.useFormatterDuringInput {
  328. let unsafePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
  329. defer {
  330. unsafePointer.deallocate()
  331. }
  332. let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(unsafePointer)
  333. let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
  334. if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
  335. row.value = value.pointee as? T
  336. guard var selStartPos = textField.selectedTextRange?.start else { return }
  337. let oldVal = textField.text
  338. textField.text = row.displayValueFor?(row.value)
  339. selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) ?? selStartPos
  340. textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos)
  341. return
  342. }
  343. } else {
  344. let unsafePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
  345. defer {
  346. unsafePointer.deallocate()
  347. }
  348. let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(unsafePointer)
  349. let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
  350. if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
  351. row.value = value.pointee as? T
  352. } else {
  353. row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value)
  354. }
  355. }
  356. }
  357. // MARK: Helpers
  358. private func setupTitleLabel() {
  359. titleLabel = self.textLabel
  360. titleLabel?.translatesAutoresizingMaskIntoConstraints = false
  361. titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal)
  362. titleLabel?.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .horizontal)
  363. }
  364. private func displayValue(useFormatter: Bool) -> String? {
  365. guard let v = row.value else { return nil }
  366. if let formatter = (row as? FormatterConformance)?.formatter, useFormatter {
  367. return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v)
  368. }
  369. return String(describing: v)
  370. }
  371. // MARK: TextFieldDelegate
  372. open func textFieldDidBeginEditing(_ textField: UITextField) {
  373. formViewController()?.beginEditing(of: self)
  374. formViewController()?.textInputDidBeginEditing(textField, cell: self)
  375. if let fieldRowConformance = row as? FormatterConformance, let _ = fieldRowConformance.formatter, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput {
  376. textField.text = displayValue(useFormatter: true)
  377. } else {
  378. textField.text = displayValue(useFormatter: false)
  379. }
  380. }
  381. open func textFieldDidEndEditing(_ textField: UITextField) {
  382. formViewController()?.endEditing(of: self)
  383. formViewController()?.textInputDidEndEditing(textField, cell: self)
  384. textFieldDidChange(textField)
  385. textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil)
  386. }
  387. open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  388. return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true
  389. }
  390. open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  391. return formViewController()?.textInput(textField, shouldChangeCharactersInRange:range, replacementString:string, cell: self) ?? true
  392. }
  393. open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
  394. return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true
  395. }
  396. open func textFieldShouldClear(_ textField: UITextField) -> Bool {
  397. return formViewController()?.textInputShouldClear(textField, cell: self) ?? true
  398. }
  399. open func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
  400. return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
  401. }
  402. open override func layoutSubviews() {
  403. super.layoutSubviews()
  404. guard let row = (row as? FieldRowConformance) else { return }
  405. defer {
  406. // As titleLabel is the textLabel, iOS may re-layout without updating constraints, for example:
  407. // swiping, showing alert or actionsheet from the same section.
  408. // thus we need forcing update to use customConstraints()
  409. setNeedsUpdateConstraints()
  410. updateConstraintsIfNeeded()
  411. }
  412. guard let titlePercentage = row.titlePercentage else { return }
  413. var targetTitleWidth = bounds.size.width * titlePercentage
  414. if let imageView = imageView, let _ = imageView.image, let titleLabel = titleLabel {
  415. var extraWidthToSubtract = titleLabel.frame.minX - imageView.frame.minX // Left-to-right interface layout
  416. if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft {
  417. extraWidthToSubtract = imageView.frame.maxX - titleLabel.frame.maxX
  418. }
  419. targetTitleWidth -= extraWidthToSubtract
  420. }
  421. calculatedTitlePercentage = targetTitleWidth / contentView.bounds.size.width
  422. }
  423. }