Core.swift 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. // Core.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. // MARK: Row
  27. internal class RowDefaults {
  28. static var cellUpdate = [String: (BaseCell, BaseRow) -> Void]()
  29. static var cellSetup = [String: (BaseCell, BaseRow) -> Void]()
  30. static var onCellHighlightChanged = [String: (BaseCell, BaseRow) -> Void]()
  31. static var rowInitialization = [String: (BaseRow) -> Void]()
  32. static var onRowValidationChanged = [String: (BaseCell, BaseRow) -> Void]()
  33. static var rawCellUpdate = [String: Any]()
  34. static var rawCellSetup = [String: Any]()
  35. static var rawOnCellHighlightChanged = [String: Any]()
  36. static var rawRowInitialization = [String: Any]()
  37. static var rawOnRowValidationChanged = [String: Any]()
  38. }
  39. // MARK: FormCells
  40. public struct CellProvider<Cell: BaseCell> where Cell: CellType {
  41. /// Nibname of the cell that will be created.
  42. public private (set) var nibName: String?
  43. /// Bundle from which to get the nib file.
  44. public private (set) var bundle: Bundle!
  45. public init() {}
  46. public init(nibName: String, bundle: Bundle? = nil) {
  47. self.nibName = nibName
  48. self.bundle = bundle ?? Bundle(for: Cell.self)
  49. }
  50. /**
  51. Creates the cell with the specified style.
  52. - parameter cellStyle: The style with which the cell will be created.
  53. - returns: the cell
  54. */
  55. func makeCell(style: UITableViewCell.CellStyle) -> Cell {
  56. if let nibName = self.nibName {
  57. return bundle.loadNibNamed(nibName, owner: nil, options: nil)!.first as! Cell
  58. }
  59. return Cell.init(style: style, reuseIdentifier: nil)
  60. }
  61. }
  62. /**
  63. Enumeration that defines how a controller should be created.
  64. - Callback->VCType: Creates the controller inside the specified block
  65. - NibFile: Loads a controller from a nib file in some bundle
  66. - StoryBoard: Loads the controller from a Storyboard by its storyboard id
  67. */
  68. public enum ControllerProvider<VCType: UIViewController> {
  69. /**
  70. * Creates the controller inside the specified block
  71. */
  72. case callback(builder: (() -> VCType))
  73. /**
  74. * Loads a controller from a nib file in some bundle
  75. */
  76. case nibFile(name: String, bundle: Bundle?)
  77. /**
  78. * Loads the controller from a Storyboard by its storyboard id
  79. */
  80. case storyBoard(storyboardId: String, storyboardName: String, bundle: Bundle?)
  81. func makeController() -> VCType {
  82. switch self {
  83. case .callback(let builder):
  84. return builder()
  85. case .nibFile(let nibName, let bundle):
  86. return VCType.init(nibName: nibName, bundle:bundle ?? Bundle(for: VCType.self))
  87. case .storyBoard(let storyboardId, let storyboardName, let bundle):
  88. let sb = UIStoryboard(name: storyboardName, bundle: bundle ?? Bundle(for: VCType.self))
  89. return sb.instantiateViewController(withIdentifier: storyboardId) as! VCType
  90. }
  91. }
  92. }
  93. /**
  94. Defines how a controller should be presented.
  95. - Show?: Shows the controller with `showViewController(...)`.
  96. - PresentModally?: Presents the controller modally.
  97. - SegueName?: Performs the segue with the specified identifier (name).
  98. - SegueClass?: Performs a segue from a segue class.
  99. */
  100. public enum PresentationMode<VCType: UIViewController> {
  101. /**
  102. * Shows the controller, created by the specified provider, with `showViewController(...)`.
  103. */
  104. case show(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
  105. /**
  106. * Presents the controller, created by the specified provider, modally.
  107. */
  108. case presentModally(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
  109. /**
  110. * Performs the segue with the specified identifier (name).
  111. */
  112. case segueName(segueName: String, onDismiss: ((UIViewController) -> Void)?)
  113. /**
  114. * Performs a segue from a segue class.
  115. */
  116. case segueClass(segueClass: UIStoryboardSegue.Type, onDismiss: ((UIViewController) -> Void)?)
  117. case popover(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
  118. public var onDismissCallback: ((UIViewController) -> Void)? {
  119. switch self {
  120. case .show(_, let completion):
  121. return completion
  122. case .presentModally(_, let completion):
  123. return completion
  124. case .segueName(_, let completion):
  125. return completion
  126. case .segueClass(_, let completion):
  127. return completion
  128. case .popover(_, let completion):
  129. return completion
  130. }
  131. }
  132. /**
  133. Present the view controller provided by PresentationMode. Should only be used from custom row implementation.
  134. - parameter viewController: viewController to present if it makes sense (normally provided by makeController method)
  135. - parameter row: associated row
  136. - parameter presentingViewController: form view controller
  137. */
  138. public func present(_ viewController: VCType!, row: BaseRow, presentingController: FormViewController) {
  139. switch self {
  140. case .show(_, _):
  141. presentingController.show(viewController, sender: row)
  142. case .presentModally(_, _):
  143. presentingController.present(viewController, animated: true)
  144. case .segueName(let segueName, _):
  145. presentingController.performSegue(withIdentifier: segueName, sender: row)
  146. case .segueClass(let segueClass, _):
  147. let segue = segueClass.init(identifier: row.tag, source: presentingController, destination: viewController)
  148. presentingController.prepare(for: segue, sender: row)
  149. segue.perform()
  150. case .popover(_, _):
  151. guard let porpoverController = viewController.popoverPresentationController else {
  152. fatalError()
  153. }
  154. porpoverController.sourceView = porpoverController.sourceView ?? presentingController.tableView
  155. presentingController.present(viewController, animated: true)
  156. }
  157. }
  158. /**
  159. Creates the view controller specified by presentation mode. Should only be used from custom row implementation.
  160. - returns: the created view controller or nil depending on the PresentationMode type.
  161. */
  162. public func makeController() -> VCType? {
  163. switch self {
  164. case .show(let controllerProvider, let completionCallback):
  165. let controller = controllerProvider.makeController()
  166. let completionController = controller as? RowControllerType
  167. if let callback = completionCallback {
  168. completionController?.onDismissCallback = callback
  169. }
  170. return controller
  171. case .presentModally(let controllerProvider, let completionCallback):
  172. let controller = controllerProvider.makeController()
  173. let completionController = controller as? RowControllerType
  174. if let callback = completionCallback {
  175. completionController?.onDismissCallback = callback
  176. }
  177. return controller
  178. case .popover(let controllerProvider, let completionCallback):
  179. let controller = controllerProvider.makeController()
  180. controller.modalPresentationStyle = .popover
  181. let completionController = controller as? RowControllerType
  182. if let callback = completionCallback {
  183. completionController?.onDismissCallback = callback
  184. }
  185. return controller
  186. default:
  187. return nil
  188. }
  189. }
  190. }
  191. /**
  192. * Protocol to be implemented by custom formatters.
  193. */
  194. public protocol FormatterProtocol {
  195. func getNewPosition(forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition
  196. }
  197. // MARK: Predicate Machine
  198. enum ConditionType {
  199. case hidden, disabled
  200. }
  201. /**
  202. Enumeration that are used to specify the disbaled and hidden conditions of rows
  203. - Function: A function that calculates the result
  204. - Predicate: A predicate that returns the result
  205. */
  206. public enum Condition {
  207. /**
  208. * Calculate the condition inside a block
  209. *
  210. * @param Array of tags of the rows this function depends on
  211. * @param Form->Bool The block that calculates the result
  212. *
  213. * @return If the condition is true or false
  214. */
  215. case function([String], (Form)->Bool)
  216. /**
  217. * Calculate the condition using a NSPredicate
  218. *
  219. * @param NSPredicate The predicate that will be evaluated
  220. *
  221. * @return If the condition is true or false
  222. */
  223. case predicate(NSPredicate)
  224. }
  225. extension Condition : ExpressibleByBooleanLiteral {
  226. /**
  227. Initialize a condition to return afixed boolean value always
  228. */
  229. public init(booleanLiteral value: Bool) {
  230. self = Condition.function([]) { _ in return value }
  231. }
  232. }
  233. extension Condition : ExpressibleByStringLiteral {
  234. /**
  235. Initialize a Condition with a string that will be converted to a NSPredicate
  236. */
  237. public init(stringLiteral value: String) {
  238. self = .predicate(NSPredicate(format: value))
  239. }
  240. /**
  241. Initialize a Condition with a string that will be converted to a NSPredicate
  242. */
  243. public init(unicodeScalarLiteral value: String) {
  244. self = .predicate(NSPredicate(format: value))
  245. }
  246. /**
  247. Initialize a Condition with a string that will be converted to a NSPredicate
  248. */
  249. public init(extendedGraphemeClusterLiteral value: String) {
  250. self = .predicate(NSPredicate(format: value))
  251. }
  252. }
  253. // MARK: Errors
  254. /**
  255. Errors thrown by Eureka
  256. - duplicatedTag: When a section or row is inserted whose tag dows already exist
  257. - rowNotInSection: When a row was expected to be in a Section, but is not.
  258. */
  259. public enum EurekaError: Error {
  260. case duplicatedTag(tag: String)
  261. case rowNotInSection(row: BaseRow)
  262. }
  263. //Mark: FormViewController
  264. /**
  265. * A protocol implemented by FormViewController
  266. */
  267. public protocol FormViewControllerProtocol {
  268. var tableView: UITableView! { get }
  269. func beginEditing<T>(of: Cell<T>)
  270. func endEditing<T>(of: Cell<T>)
  271. func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation
  272. func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation
  273. func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation
  274. func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation
  275. func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation
  276. func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation
  277. }
  278. /**
  279. * Navigation options for a form view controller.
  280. */
  281. public struct RowNavigationOptions: OptionSet {
  282. private enum NavigationOptions: Int {
  283. case disabled = 0, enabled = 1, stopDisabledRow = 2, skipCanNotBecomeFirstResponderRow = 4
  284. }
  285. public let rawValue: Int
  286. public init(rawValue: Int) { self.rawValue = rawValue}
  287. private init(_ options: NavigationOptions ) { self.rawValue = options.rawValue }
  288. /// No navigation.
  289. public static let Disabled = RowNavigationOptions(.disabled)
  290. /// Full navigation.
  291. public static let Enabled = RowNavigationOptions(.enabled)
  292. /// Break navigation when next row is disabled.
  293. public static let StopDisabledRow = RowNavigationOptions(.stopDisabledRow)
  294. /// Break navigation when next row cannot become first responder.
  295. public static let SkipCanNotBecomeFirstResponderRow = RowNavigationOptions(.skipCanNotBecomeFirstResponderRow)
  296. }
  297. /**
  298. * Defines the configuration for the keyboardType of FieldRows.
  299. */
  300. public struct KeyboardReturnTypeConfiguration {
  301. /// Used when the next row is available.
  302. public var nextKeyboardType = UIReturnKeyType.next
  303. /// Used if next row is not available.
  304. public var defaultKeyboardType = UIReturnKeyType.default
  305. public init() {}
  306. public init(nextKeyboardType: UIReturnKeyType, defaultKeyboardType: UIReturnKeyType) {
  307. self.nextKeyboardType = nextKeyboardType
  308. self.defaultKeyboardType = defaultKeyboardType
  309. }
  310. }
  311. /**
  312. * Options that define when an inline row should collapse.
  313. */
  314. public struct InlineRowHideOptions: OptionSet {
  315. private enum _InlineRowHideOptions: Int {
  316. case never = 0, anotherInlineRowIsShown = 1, firstResponderChanges = 2
  317. }
  318. public let rawValue: Int
  319. public init(rawValue: Int) { self.rawValue = rawValue}
  320. private init(_ options: _InlineRowHideOptions ) { self.rawValue = options.rawValue }
  321. /// Never collapse automatically. Only when user taps inline row.
  322. public static let Never = InlineRowHideOptions(.never)
  323. /// Collapse qhen another inline row expands. Just one inline row will be expanded at a time.
  324. public static let AnotherInlineRowIsShown = InlineRowHideOptions(.anotherInlineRowIsShown)
  325. /// Collapse when first responder changes.
  326. public static let FirstResponderChanges = InlineRowHideOptions(.firstResponderChanges)
  327. }
  328. /// View controller that shows a form.
  329. @objc(EurekaFormViewController)
  330. open class FormViewController: UIViewController, FormViewControllerProtocol, FormDelegate {
  331. @IBOutlet public var tableView: UITableView!
  332. private lazy var _form: Form = { [weak self] in
  333. let form = Form()
  334. form.delegate = self
  335. return form
  336. }()
  337. public var form: Form {
  338. get { return _form }
  339. set {
  340. guard form !== newValue else { return }
  341. _form.delegate = nil
  342. tableView?.endEditing(false)
  343. _form = newValue
  344. _form.delegate = self
  345. if isViewLoaded {
  346. tableView?.reloadData()
  347. }
  348. }
  349. }
  350. /// Extra space to leave between between the row in focus and the keyboard
  351. open var rowKeyboardSpacing: CGFloat = 0
  352. /// Enables animated scrolling on row navigation
  353. open var animateScroll = false
  354. /// The default scroll position on the focussed cell when keyboard appears
  355. open var defaultScrollPosition = UITableView.ScrollPosition.none
  356. /// Accessory view that is responsible for the navigation between rows
  357. private var navigationAccessoryView: (UIView & NavigationAccessory)!
  358. /// Custom Accesory View to be used as a replacement
  359. open var customNavigationAccessoryView: (UIView & NavigationAccessory)? {
  360. return nil
  361. }
  362. /// Defines the behaviour of the navigation between rows
  363. public var navigationOptions: RowNavigationOptions?
  364. public var tableViewStyle: UITableView.Style = .grouped
  365. public init(style: UITableView.Style) {
  366. super.init(nibName: nil, bundle: nil)
  367. tableViewStyle = style
  368. }
  369. public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  370. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  371. }
  372. public required init?(coder aDecoder: NSCoder) {
  373. super.init(coder: aDecoder)
  374. }
  375. open override func viewDidLoad() {
  376. super.viewDidLoad()
  377. navigationAccessoryView = customNavigationAccessoryView ?? NavigationAccessoryView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 44.0))
  378. navigationAccessoryView.autoresizingMask = .flexibleWidth
  379. if tableView == nil {
  380. tableView = UITableView(frame: view.bounds, style: tableViewStyle)
  381. tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  382. tableView.cellLayoutMarginsFollowReadableWidth = false
  383. }
  384. if tableView.superview == nil {
  385. view.addSubview(tableView)
  386. }
  387. if tableView.delegate == nil {
  388. tableView.delegate = self
  389. }
  390. if tableView.dataSource == nil {
  391. tableView.dataSource = self
  392. }
  393. tableView.rowHeight = UITableView.automaticDimension
  394. tableView.estimatedRowHeight = BaseRow.estimatedRowHeight
  395. tableView.allowsSelectionDuringEditing = true
  396. }
  397. open override func viewWillAppear(_ animated: Bool) {
  398. super.viewWillAppear(animated)
  399. animateTableView = true
  400. let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? []
  401. if !selectedIndexPaths.isEmpty {
  402. if #available(iOS 13.0, *) {
  403. if tableView.window != nil {
  404. tableView.reloadRows(at: selectedIndexPaths, with: .none)
  405. }
  406. } else {
  407. tableView.reloadRows(at: selectedIndexPaths, with: .none)
  408. }
  409. }
  410. selectedIndexPaths.forEach {
  411. tableView.selectRow(at: $0, animated: false, scrollPosition: .none)
  412. }
  413. let deselectionAnimation = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in
  414. selectedIndexPaths.forEach {
  415. self?.tableView.deselectRow(at: $0, animated: context.isAnimated)
  416. }
  417. }
  418. let reselection = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in
  419. if context.isCancelled {
  420. selectedIndexPaths.forEach {
  421. self?.tableView.selectRow(at: $0, animated: false, scrollPosition: .none)
  422. }
  423. }
  424. }
  425. if let coordinator = transitionCoordinator {
  426. coordinator.animate(alongsideTransition: deselectionAnimation, completion: reselection)
  427. } else {
  428. selectedIndexPaths.forEach {
  429. tableView.deselectRow(at: $0, animated: false)
  430. }
  431. }
  432. NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  433. NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  434. if form.containsMultivaluedSection && (isBeingPresented || isMovingToParent) {
  435. tableView.setEditing(true, animated: false)
  436. }
  437. }
  438. open override func viewWillDisappear(_ animated: Bool) {
  439. super.viewWillDisappear(animated)
  440. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
  441. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
  442. }
  443. open override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  444. super.prepare(for: segue, sender: sender)
  445. let baseRow = sender as? BaseRow
  446. baseRow?.prepare(for: segue)
  447. }
  448. /**
  449. Returns the navigation accessory view if it is enabled. Returns nil otherwise.
  450. */
  451. open func inputAccessoryView(for row: BaseRow) -> UIView? {
  452. let options = navigationOptions ?? Form.defaultNavigationOptions
  453. guard options.contains(.Enabled) else { return nil }
  454. guard row.baseCell.cellCanBecomeFirstResponder() else { return nil}
  455. navigationAccessoryView.previousEnabled = nextRow(for: row, withDirection: .up) != nil
  456. navigationAccessoryView.doneClosure = { [weak self] in
  457. self?.navigationDone()
  458. }
  459. navigationAccessoryView.previousClosure = { [weak self] in
  460. self?.navigationPrevious()
  461. }
  462. navigationAccessoryView.nextClosure = { [weak self] in
  463. self?.navigationNext()
  464. }
  465. navigationAccessoryView.nextEnabled = nextRow(for: row, withDirection: .down) != nil
  466. return navigationAccessoryView
  467. }
  468. // MARK: FormViewControllerProtocol
  469. /**
  470. Called when a cell becomes first responder
  471. */
  472. public final func beginEditing<T>(of cell: Cell<T>) {
  473. cell.row.isHighlighted = true
  474. cell.row.updateCell()
  475. RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row)
  476. cell.row.callbackOnCellHighlightChanged?()
  477. guard let _ = tableView, (form.inlineRowHideOptions ?? Form.defaultInlineRowHideOptions).contains(.FirstResponderChanges) else { return }
  478. let row = cell.baseRow
  479. let inlineRow = row?._inlineRow
  480. for row in form.allRows.filter({ $0 !== row && $0 !== inlineRow && $0._inlineRow != nil }) {
  481. if let inlineRow = row as? BaseInlineRowType {
  482. inlineRow.collapseInlineRow()
  483. }
  484. }
  485. }
  486. /**
  487. Called when a cell resigns first responder
  488. */
  489. public final func endEditing<T>(of cell: Cell<T>) {
  490. cell.row.isHighlighted = false
  491. cell.row.wasBlurred = true
  492. RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row)
  493. cell.row.callbackOnCellHighlightChanged?()
  494. if cell.row.validationOptions.contains(.validatesOnBlur) || (cell.row.wasChanged && cell.row.validationOptions.contains(.validatesOnChangeAfterBlurred)) {
  495. cell.row.validate()
  496. }
  497. cell.row.updateCell()
  498. }
  499. /**
  500. Returns the animation for the insertion of the given rows.
  501. */
  502. open func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation {
  503. return .fade
  504. }
  505. /**
  506. Returns the animation for the deletion of the given rows.
  507. */
  508. open func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation {
  509. return .fade
  510. }
  511. /**
  512. Returns the animation for the reloading of the given rows.
  513. */
  514. open func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation {
  515. return .automatic
  516. }
  517. /**
  518. Returns the animation for the insertion of the given sections.
  519. */
  520. open func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation {
  521. return .automatic
  522. }
  523. /**
  524. Returns the animation for the deletion of the given sections.
  525. */
  526. open func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation {
  527. return .automatic
  528. }
  529. /**
  530. Returns the animation for the reloading of the given sections.
  531. */
  532. open func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation {
  533. return .automatic
  534. }
  535. // MARK: TextField and TextView Delegate
  536. open func textInputShouldBeginEditing<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
  537. return true
  538. }
  539. open func textInputDidBeginEditing<T>(_ textInput: UITextInput, cell: Cell<T>) {
  540. if let row = cell.row as? KeyboardReturnHandler {
  541. let next = nextRow(for: cell.row, withDirection: .down)
  542. if let textField = textInput as? UITextField {
  543. textField.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ??
  544. (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) :
  545. (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ??
  546. Form.defaultKeyboardReturnType.defaultKeyboardType))
  547. } else if let textView = textInput as? UITextView {
  548. textView.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ??
  549. (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) :
  550. (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ??
  551. Form.defaultKeyboardReturnType.defaultKeyboardType))
  552. }
  553. }
  554. }
  555. open func textInputShouldEndEditing<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
  556. return true
  557. }
  558. open func textInputDidEndEditing<T>(_ textInput: UITextInput, cell: Cell<T>) {
  559. }
  560. open func textInput<T>(_ textInput: UITextInput, shouldChangeCharactersInRange range: NSRange, replacementString string: String, cell: Cell<T>) -> Bool {
  561. return true
  562. }
  563. open func textInputShouldClear<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
  564. return true
  565. }
  566. open func textInputShouldReturn<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
  567. if let nextRow = nextRow(for: cell.row, withDirection: .down) {
  568. if nextRow.baseCell.cellCanBecomeFirstResponder() {
  569. nextRow.baseCell.cellBecomeFirstResponder()
  570. return true
  571. }
  572. }
  573. tableView?.endEditing(true)
  574. return true
  575. }
  576. // MARK: FormDelegate
  577. open func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?) {}
  578. // MARK: UITableViewDelegate
  579. @objc open func tableView(_ tableView: UITableView, willBeginReorderingRowAtIndexPath indexPath: IndexPath) {
  580. // end editing if inline cell is first responder
  581. let row = form[indexPath]
  582. if let inlineRow = row as? BaseInlineRowType, row._inlineRow != nil {
  583. inlineRow.collapseInlineRow()
  584. }
  585. }
  586. // MARK: FormDelegate
  587. open func sectionsHaveBeenAdded(_ sections: [Section], at indexes: IndexSet) {
  588. guard animateTableView else { return }
  589. tableView?.beginUpdates()
  590. tableView?.insertSections(indexes, with: insertAnimation(forSections: sections))
  591. tableView?.endUpdates()
  592. }
  593. open func sectionsHaveBeenRemoved(_ sections: [Section], at indexes: IndexSet) {
  594. guard animateTableView else { return }
  595. tableView?.beginUpdates()
  596. tableView?.deleteSections(indexes, with: deleteAnimation(forSections: sections))
  597. tableView?.endUpdates()
  598. }
  599. open func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at indexes: IndexSet) {
  600. guard animateTableView else { return }
  601. tableView?.beginUpdates()
  602. tableView?.reloadSections(indexes, with: reloadAnimation(oldSections: oldSections, newSections: newSections))
  603. tableView?.endUpdates()
  604. }
  605. open func rowsHaveBeenAdded(_ rows: [BaseRow], at indexes: [IndexPath]) {
  606. guard animateTableView else { return }
  607. tableView?.beginUpdates()
  608. tableView?.insertRows(at: indexes, with: insertAnimation(forRows: rows))
  609. tableView?.endUpdates()
  610. }
  611. open func rowsHaveBeenRemoved(_ rows: [BaseRow], at indexes: [IndexPath]) {
  612. guard animateTableView else { return }
  613. tableView?.beginUpdates()
  614. tableView?.deleteRows(at: indexes, with: deleteAnimation(forRows: rows))
  615. tableView?.endUpdates()
  616. }
  617. open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at indexes: [IndexPath]) {
  618. guard animateTableView else { return }
  619. tableView?.beginUpdates()
  620. tableView?.reloadRows(at: indexes, with: reloadAnimation(oldRows: oldRows, newRows: newRows))
  621. tableView?.endUpdates()
  622. }
  623. // MARK: Private
  624. var oldBottomInset: CGFloat?
  625. var animateTableView = false
  626. /** Calculates the height needed for a header or footer. */
  627. fileprivate func height(specifiedHeight: (() -> CGFloat)?, sectionView: UIView?, sectionTitle: String?) -> CGFloat {
  628. if let height = specifiedHeight {
  629. return height()
  630. }
  631. if let sectionView = sectionView {
  632. let height = sectionView.bounds.height
  633. if height == 0 {
  634. return UITableView.automaticDimension
  635. }
  636. return height
  637. }
  638. if let sectionTitle = sectionTitle,
  639. sectionTitle != "" {
  640. return UITableView.automaticDimension
  641. }
  642. // Fix for iOS 11+. By returning 0, we ensure that no section header or
  643. // footer is shown when self-sizing is enabled (i.e. when
  644. // tableView.estimatedSectionHeaderHeight or tableView.estimatedSectionFooterHeight
  645. // == UITableView.automaticDimension).
  646. if tableView.style == .plain {
  647. return 0
  648. }
  649. return UITableView.automaticDimension
  650. }
  651. }
  652. extension FormViewController : UITableViewDelegate {
  653. // MARK: UITableViewDelegate
  654. open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  655. return indexPath
  656. }
  657. open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  658. guard tableView == self.tableView else { return }
  659. let row = form[indexPath]
  660. // row.baseCell.cellBecomeFirstResponder() may be cause InlineRow collapsed then section count will be changed. Use orignal indexPath will out of section's bounds.
  661. if !row.baseCell.cellCanBecomeFirstResponder() || !row.baseCell.cellBecomeFirstResponder() {
  662. self.tableView?.endEditing(true)
  663. }
  664. row.didSelect()
  665. }
  666. open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  667. guard tableView == self.tableView else { return tableView.rowHeight }
  668. let row = form[indexPath.section][indexPath.row]
  669. return row.baseCell.height?() ?? tableView.rowHeight
  670. }
  671. open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  672. guard tableView == self.tableView else { return tableView.estimatedRowHeight }
  673. let row = form[indexPath.section][indexPath.row]
  674. return row.baseCell.height?() ?? tableView.estimatedRowHeight
  675. }
  676. open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  677. return form[section].header?.viewForSection(form[section], type: .header)
  678. }
  679. open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
  680. return form[section].footer?.viewForSection(form[section], type:.footer)
  681. }
  682. open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  683. return height(specifiedHeight: form[section].header?.height,
  684. sectionView: self.tableView(tableView, viewForHeaderInSection: section),
  685. sectionTitle: self.tableView(tableView, titleForHeaderInSection: section))
  686. }
  687. open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
  688. return height(specifiedHeight: form[section].footer?.height,
  689. sectionView: self.tableView(tableView, viewForFooterInSection: section),
  690. sectionTitle: self.tableView(tableView, titleForFooterInSection: section))
  691. }
  692. open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  693. let row = form[indexPath]
  694. guard !row.isDisabled else { return false }
  695. if row.trailingSwipe.actions.count > 0 { return true }
  696. if #available(iOS 11,*), row.leadingSwipe.actions.count > 0 { return true }
  697. guard let section = form[indexPath.section] as? BaseMultivaluedSection else { return false }
  698. guard !(indexPath.row == section.count - 1 && section.multivaluedOptions.contains(.Insert) && section.showInsertIconInAddButton) else {
  699. return true
  700. }
  701. if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil {
  702. return false
  703. }
  704. return true
  705. }
  706. open func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  707. if editingStyle == .delete {
  708. let row = form[indexPath]
  709. let section = row.section!
  710. if let _ = row.baseCell.findFirstResponder() {
  711. tableView.endEditing(true)
  712. }
  713. section.remove(at: indexPath.row)
  714. } else if editingStyle == .insert {
  715. guard var section = form[indexPath.section] as? BaseMultivaluedSection else { return }
  716. guard let multivaluedRowToInsertAt = section.multivaluedRowToInsertAt else {
  717. fatalError("Multivalued section multivaluedRowToInsertAt property must be set up")
  718. }
  719. let newRow = multivaluedRowToInsertAt(max(0, section.count - 1))
  720. section.insert(newRow, at: max(0, section.count - 1))
  721. DispatchQueue.main.async {
  722. tableView.isEditing = !tableView.isEditing
  723. tableView.isEditing = !tableView.isEditing
  724. }
  725. tableView.scrollToRow(at: IndexPath(row: section.count - 1, section: indexPath.section), at: .bottom, animated: true)
  726. if newRow.baseCell.cellCanBecomeFirstResponder() {
  727. newRow.baseCell.cellBecomeFirstResponder()
  728. } else if let inlineRow = newRow as? BaseInlineRowType {
  729. inlineRow.expandInlineRow()
  730. }
  731. }
  732. }
  733. open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  734. guard let section = form[indexPath.section] as? BaseMultivaluedSection, section.multivaluedOptions.contains(.Reorder) && section.count > 1 else {
  735. return false
  736. }
  737. if section.multivaluedOptions.contains(.Insert) && (section.count <= 2 || indexPath.row == (section.count - 1)) {
  738. return false
  739. }
  740. if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil {
  741. return false
  742. }
  743. return true
  744. }
  745. open func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  746. guard let section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return sourceIndexPath }
  747. guard sourceIndexPath.section == proposedDestinationIndexPath.section else { return sourceIndexPath }
  748. let destRow = form[proposedDestinationIndexPath]
  749. if destRow is BaseInlineRowType && destRow._inlineRow != nil {
  750. return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section)
  751. }
  752. if proposedDestinationIndexPath.row > 0 {
  753. let previousRow = form[IndexPath(row: proposedDestinationIndexPath.row - 1, section: proposedDestinationIndexPath.section)]
  754. if previousRow is BaseInlineRowType && previousRow._inlineRow != nil {
  755. return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section)
  756. }
  757. }
  758. if section.multivaluedOptions.contains(.Insert) && proposedDestinationIndexPath.row == section.count - 1 {
  759. return IndexPath(row: section.count - 2, section: sourceIndexPath.section)
  760. }
  761. return proposedDestinationIndexPath
  762. }
  763. open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  764. guard var section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return }
  765. if sourceIndexPath.row < section.count && destinationIndexPath.row < section.count && sourceIndexPath.row != destinationIndexPath.row {
  766. let sourceRow = form[sourceIndexPath]
  767. animateTableView = false
  768. section.remove(at: sourceIndexPath.row)
  769. section.insert(sourceRow, at: destinationIndexPath.row)
  770. animateTableView = true
  771. // update the accessory view
  772. let _ = inputAccessoryView(for: sourceRow)
  773. }
  774. }
  775. open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
  776. guard let section = form[indexPath.section] as? BaseMultivaluedSection else {
  777. if form[indexPath].trailingSwipe.actions.count > 0 {
  778. return .delete
  779. }
  780. return .none
  781. }
  782. if section.multivaluedOptions.contains(.Insert) && indexPath.row == section.count - 1 {
  783. return section.showInsertIconInAddButton ? .insert : .none
  784. }
  785. if section.multivaluedOptions.contains(.Delete) {
  786. return .delete
  787. }
  788. return .none
  789. }
  790. open func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
  791. return self.tableView(tableView, editingStyleForRowAt: indexPath) != .none
  792. }
  793. @available(iOS 11,*)
  794. open func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  795. guard !form[indexPath].leadingSwipe.actions.isEmpty else {
  796. return nil
  797. }
  798. return form[indexPath].leadingSwipe.contextualConfiguration
  799. }
  800. @available(iOS 11,*)
  801. open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  802. guard !form[indexPath].trailingSwipe.actions.isEmpty else {
  803. return nil
  804. }
  805. return form[indexPath].trailingSwipe.contextualConfiguration
  806. }
  807. @available(macCatalyst, deprecated: 13.1, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead")
  808. @available(iOS, deprecated: 13, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead")
  809. open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?{
  810. guard let actions = form[indexPath].trailingSwipe.contextualActions as? [UITableViewRowAction], !actions.isEmpty else {
  811. return nil
  812. }
  813. return actions
  814. }
  815. }
  816. extension FormViewController : UITableViewDataSource {
  817. // MARK: UITableViewDataSource
  818. open func numberOfSections(in tableView: UITableView) -> Int {
  819. return form.count
  820. }
  821. open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  822. return form[section].count
  823. }
  824. open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  825. form[indexPath].updateCell()
  826. return form[indexPath].baseCell
  827. }
  828. open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  829. return form[section].header?.title
  830. }
  831. open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  832. return form[section].footer?.title
  833. }
  834. open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
  835. return nil
  836. }
  837. open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
  838. return 0
  839. }
  840. }
  841. extension FormViewController : UIScrollViewDelegate {
  842. // MARK: UIScrollViewDelegate
  843. open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  844. guard let tableView = tableView, scrollView === tableView else { return }
  845. tableView.endEditing(true)
  846. }
  847. }
  848. extension FormViewController {
  849. // MARK: KeyBoard Notifications
  850. /**
  851. Called when the keyboard will appear. Adjusts insets of the tableView and scrolls it if necessary.
  852. */
  853. @objc open func keyboardWillShow(_ notification: Notification) {
  854. guard let table = tableView, let cell = table.findFirstResponder()?.formCell() else { return }
  855. let keyBoardInfo = notification.userInfo!
  856. let endFrame = keyBoardInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue
  857. let keyBoardFrame = table.window!.convert(endFrame.cgRectValue, to: table.superview)
  858. var newBottomInset = table.frame.origin.y + table.frame.size.height - keyBoardFrame.origin.y + rowKeyboardSpacing
  859. if #available(iOS 11.0, *) {
  860. newBottomInset = newBottomInset - tableView.safeAreaInsets.bottom
  861. }
  862. var tableInsets = table.contentInset
  863. var scrollIndicatorInsets = table.scrollIndicatorInsets
  864. oldBottomInset = oldBottomInset ?? tableInsets.bottom
  865. if newBottomInset > oldBottomInset! {
  866. tableInsets.bottom = newBottomInset
  867. scrollIndicatorInsets.bottom = tableInsets.bottom
  868. UIView.beginAnimations(nil, context: nil)
  869. UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double))
  870. UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!)
  871. table.contentInset = tableInsets
  872. table.scrollIndicatorInsets = scrollIndicatorInsets
  873. if let selectedRow = table.indexPath(for: cell) {
  874. if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 11 {
  875. let rect = table.rectForRow(at: selectedRow)
  876. table.scrollRectToVisible(rect, animated: animateScroll)
  877. } else {
  878. table.scrollToRow(at: selectedRow, at: defaultScrollPosition, animated: animateScroll)
  879. }
  880. }
  881. UIView.commitAnimations()
  882. }
  883. }
  884. /**
  885. Called when the keyboard will disappear. Adjusts insets of the tableView.
  886. */
  887. @objc open func keyboardWillHide(_ notification: Notification) {
  888. guard let table = tableView, let oldBottom = oldBottomInset else { return }
  889. let keyBoardInfo = notification.userInfo!
  890. var tableInsets = table.contentInset
  891. var scrollIndicatorInsets = table.scrollIndicatorInsets
  892. tableInsets.bottom = oldBottom
  893. scrollIndicatorInsets.bottom = tableInsets.bottom
  894. oldBottomInset = nil
  895. UIView.beginAnimations(nil, context: nil)
  896. UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double))
  897. UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!)
  898. table.contentInset = tableInsets
  899. table.scrollIndicatorInsets = scrollIndicatorInsets
  900. UIView.commitAnimations()
  901. }
  902. }
  903. public enum Direction { case up, down }
  904. extension FormViewController {
  905. // MARK: Navigation Methods
  906. @objc func navigationDone() {
  907. tableView?.endEditing(true)
  908. }
  909. @objc func navigationPrevious() {
  910. navigateTo(direction: .up)
  911. }
  912. @objc func navigationNext() {
  913. navigateTo(direction: .down)
  914. }
  915. open override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
  916. var didHandleEvent = false
  917. for press in presses {
  918. guard let key = press.key,
  919. key.keyCode == .keyboardTab,
  920. !key.modifierFlags.contains(.command) else { continue }
  921. if key.modifierFlags.contains(.shift) {
  922. navigateTo(direction: .up)
  923. } else {
  924. navigateTo(direction: .down)
  925. }
  926. didHandleEvent = true
  927. }
  928. if !didHandleEvent {
  929. // Didn't handle this key press, so pass the event to the next responder.
  930. super.pressesBegan(presses, with: event)
  931. }
  932. }
  933. public func navigateTo(direction: Direction) {
  934. guard let currentCell = tableView?.findFirstResponder()?.formCell() else { return }
  935. guard let currentIndexPath = tableView?.indexPath(for: currentCell) else { return }
  936. guard let nextRow = nextRow(for: form[currentIndexPath], withDirection: direction) else { return }
  937. if nextRow.baseCell.cellCanBecomeFirstResponder() {
  938. tableView?.scrollToRow(at: nextRow.indexPath!, at: .none, animated: animateScroll)
  939. nextRow.baseCell.cellBecomeFirstResponder(withDirection: direction)
  940. }
  941. }
  942. func nextRow(for currentRow: BaseRow, withDirection direction: Direction) -> BaseRow? {
  943. let options = navigationOptions ?? Form.defaultNavigationOptions
  944. guard options.contains(.Enabled) else { return nil }
  945. guard let next = direction == .down ? form.nextRow(for: currentRow) : form.previousRow(for: currentRow) else { return nil }
  946. if next.isDisabled && options.contains(.StopDisabledRow) {
  947. return nil
  948. }
  949. if !next.baseCell.cellCanBecomeFirstResponder() && !next.isDisabled && !options.contains(.SkipCanNotBecomeFirstResponderRow) {
  950. return nil
  951. }
  952. if !next.isDisabled && next.baseCell.cellCanBecomeFirstResponder() {
  953. return next
  954. }
  955. return nextRow(for: next, withDirection:direction)
  956. }
  957. }
  958. extension FormViewControllerProtocol {
  959. // MARK: Helpers
  960. func makeRowVisible(_ row: BaseRow, destinationScrollPosition: UITableView.ScrollPosition? = .bottom) {
  961. guard let destinationScrollPosition = destinationScrollPosition else { return }
  962. guard let cell = row.baseCell, let indexPath = row.indexPath, let tableView = tableView else { return }
  963. if cell.window == nil || (tableView.contentOffset.y + tableView.frame.size.height <= cell.frame.origin.y + cell.frame.size.height) {
  964. tableView.scrollToRow(at: indexPath, at: destinationScrollPosition, animated: true)
  965. }
  966. }
  967. }