1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120 |
- // Core.swift
- // Eureka ( https://github.com/xmartlabs/Eureka )
- //
- // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com )
- //
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import Foundation
- import UIKit
- // MARK: Row
- internal class RowDefaults {
- static var cellUpdate = [String: (BaseCell, BaseRow) -> Void]()
- static var cellSetup = [String: (BaseCell, BaseRow) -> Void]()
- static var onCellHighlightChanged = [String: (BaseCell, BaseRow) -> Void]()
- static var rowInitialization = [String: (BaseRow) -> Void]()
- static var onRowValidationChanged = [String: (BaseCell, BaseRow) -> Void]()
- static var rawCellUpdate = [String: Any]()
- static var rawCellSetup = [String: Any]()
- static var rawOnCellHighlightChanged = [String: Any]()
- static var rawRowInitialization = [String: Any]()
- static var rawOnRowValidationChanged = [String: Any]()
- }
- // MARK: FormCells
- public struct CellProvider<Cell: BaseCell> where Cell: CellType {
- /// Nibname of the cell that will be created.
- public private (set) var nibName: String?
- /// Bundle from which to get the nib file.
- public private (set) var bundle: Bundle!
- public init() {}
- public init(nibName: String, bundle: Bundle? = nil) {
- self.nibName = nibName
- self.bundle = bundle ?? Bundle(for: Cell.self)
- }
- /**
- Creates the cell with the specified style.
- - parameter cellStyle: The style with which the cell will be created.
- - returns: the cell
- */
- func makeCell(style: UITableViewCell.CellStyle) -> Cell {
- if let nibName = self.nibName {
- return bundle.loadNibNamed(nibName, owner: nil, options: nil)!.first as! Cell
- }
- return Cell.init(style: style, reuseIdentifier: nil)
- }
- }
- /**
- Enumeration that defines how a controller should be created.
- - Callback->VCType: Creates the controller inside the specified block
- - NibFile: Loads a controller from a nib file in some bundle
- - StoryBoard: Loads the controller from a Storyboard by its storyboard id
- */
- public enum ControllerProvider<VCType: UIViewController> {
- /**
- * Creates the controller inside the specified block
- */
- case callback(builder: (() -> VCType))
- /**
- * Loads a controller from a nib file in some bundle
- */
- case nibFile(name: String, bundle: Bundle?)
- /**
- * Loads the controller from a Storyboard by its storyboard id
- */
- case storyBoard(storyboardId: String, storyboardName: String, bundle: Bundle?)
- func makeController() -> VCType {
- switch self {
- case .callback(let builder):
- return builder()
- case .nibFile(let nibName, let bundle):
- return VCType.init(nibName: nibName, bundle:bundle ?? Bundle(for: VCType.self))
- case .storyBoard(let storyboardId, let storyboardName, let bundle):
- let sb = UIStoryboard(name: storyboardName, bundle: bundle ?? Bundle(for: VCType.self))
- return sb.instantiateViewController(withIdentifier: storyboardId) as! VCType
- }
- }
- }
- /**
- Defines how a controller should be presented.
- - Show?: Shows the controller with `showViewController(...)`.
- - PresentModally?: Presents the controller modally.
- - SegueName?: Performs the segue with the specified identifier (name).
- - SegueClass?: Performs a segue from a segue class.
- */
- public enum PresentationMode<VCType: UIViewController> {
- /**
- * Shows the controller, created by the specified provider, with `showViewController(...)`.
- */
- case show(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
- /**
- * Presents the controller, created by the specified provider, modally.
- */
- case presentModally(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
- /**
- * Performs the segue with the specified identifier (name).
- */
- case segueName(segueName: String, onDismiss: ((UIViewController) -> Void)?)
- /**
- * Performs a segue from a segue class.
- */
- case segueClass(segueClass: UIStoryboardSegue.Type, onDismiss: ((UIViewController) -> Void)?)
- case popover(controllerProvider: ControllerProvider<VCType>, onDismiss: ((UIViewController) -> Void)?)
- public var onDismissCallback: ((UIViewController) -> Void)? {
- switch self {
- case .show(_, let completion):
- return completion
- case .presentModally(_, let completion):
- return completion
- case .segueName(_, let completion):
- return completion
- case .segueClass(_, let completion):
- return completion
- case .popover(_, let completion):
- return completion
- }
- }
- /**
- Present the view controller provided by PresentationMode. Should only be used from custom row implementation.
- - parameter viewController: viewController to present if it makes sense (normally provided by makeController method)
- - parameter row: associated row
- - parameter presentingViewController: form view controller
- */
- public func present(_ viewController: VCType!, row: BaseRow, presentingController: FormViewController) {
- switch self {
- case .show(_, _):
- presentingController.show(viewController, sender: row)
- case .presentModally(_, _):
- presentingController.present(viewController, animated: true)
- case .segueName(let segueName, _):
- presentingController.performSegue(withIdentifier: segueName, sender: row)
- case .segueClass(let segueClass, _):
- let segue = segueClass.init(identifier: row.tag, source: presentingController, destination: viewController)
- presentingController.prepare(for: segue, sender: row)
- segue.perform()
- case .popover(_, _):
- guard let porpoverController = viewController.popoverPresentationController else {
- fatalError()
- }
- porpoverController.sourceView = porpoverController.sourceView ?? presentingController.tableView
- presentingController.present(viewController, animated: true)
- }
- }
- /**
- Creates the view controller specified by presentation mode. Should only be used from custom row implementation.
- - returns: the created view controller or nil depending on the PresentationMode type.
- */
- public func makeController() -> VCType? {
- switch self {
- case .show(let controllerProvider, let completionCallback):
- let controller = controllerProvider.makeController()
- let completionController = controller as? RowControllerType
- if let callback = completionCallback {
- completionController?.onDismissCallback = callback
- }
- return controller
- case .presentModally(let controllerProvider, let completionCallback):
- let controller = controllerProvider.makeController()
- let completionController = controller as? RowControllerType
- if let callback = completionCallback {
- completionController?.onDismissCallback = callback
- }
- return controller
- case .popover(let controllerProvider, let completionCallback):
- let controller = controllerProvider.makeController()
- controller.modalPresentationStyle = .popover
- let completionController = controller as? RowControllerType
- if let callback = completionCallback {
- completionController?.onDismissCallback = callback
- }
- return controller
- default:
- return nil
- }
- }
- }
- /**
- * Protocol to be implemented by custom formatters.
- */
- public protocol FormatterProtocol {
- func getNewPosition(forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition
- }
- // MARK: Predicate Machine
- enum ConditionType {
- case hidden, disabled
- }
- /**
- Enumeration that are used to specify the disbaled and hidden conditions of rows
- - Function: A function that calculates the result
- - Predicate: A predicate that returns the result
- */
- public enum Condition {
- /**
- * Calculate the condition inside a block
- *
- * @param Array of tags of the rows this function depends on
- * @param Form->Bool The block that calculates the result
- *
- * @return If the condition is true or false
- */
- case function([String], (Form)->Bool)
- /**
- * Calculate the condition using a NSPredicate
- *
- * @param NSPredicate The predicate that will be evaluated
- *
- * @return If the condition is true or false
- */
- case predicate(NSPredicate)
- }
- extension Condition : ExpressibleByBooleanLiteral {
- /**
- Initialize a condition to return afixed boolean value always
- */
- public init(booleanLiteral value: Bool) {
- self = Condition.function([]) { _ in return value }
- }
- }
- extension Condition : ExpressibleByStringLiteral {
- /**
- Initialize a Condition with a string that will be converted to a NSPredicate
- */
- public init(stringLiteral value: String) {
- self = .predicate(NSPredicate(format: value))
- }
- /**
- Initialize a Condition with a string that will be converted to a NSPredicate
- */
- public init(unicodeScalarLiteral value: String) {
- self = .predicate(NSPredicate(format: value))
- }
- /**
- Initialize a Condition with a string that will be converted to a NSPredicate
- */
- public init(extendedGraphemeClusterLiteral value: String) {
- self = .predicate(NSPredicate(format: value))
- }
- }
- // MARK: Errors
- /**
- Errors thrown by Eureka
- - duplicatedTag: When a section or row is inserted whose tag dows already exist
- - rowNotInSection: When a row was expected to be in a Section, but is not.
- */
- public enum EurekaError: Error {
- case duplicatedTag(tag: String)
- case rowNotInSection(row: BaseRow)
- }
- //Mark: FormViewController
- /**
- * A protocol implemented by FormViewController
- */
- public protocol FormViewControllerProtocol {
- var tableView: UITableView! { get }
- func beginEditing<T>(of: Cell<T>)
- func endEditing<T>(of: Cell<T>)
- func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation
- func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation
- func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation
- func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation
- func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation
- func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation
- }
- /**
- * Navigation options for a form view controller.
- */
- public struct RowNavigationOptions: OptionSet {
- private enum NavigationOptions: Int {
- case disabled = 0, enabled = 1, stopDisabledRow = 2, skipCanNotBecomeFirstResponderRow = 4
- }
- public let rawValue: Int
- public init(rawValue: Int) { self.rawValue = rawValue}
- private init(_ options: NavigationOptions ) { self.rawValue = options.rawValue }
- /// No navigation.
- public static let Disabled = RowNavigationOptions(.disabled)
- /// Full navigation.
- public static let Enabled = RowNavigationOptions(.enabled)
- /// Break navigation when next row is disabled.
- public static let StopDisabledRow = RowNavigationOptions(.stopDisabledRow)
- /// Break navigation when next row cannot become first responder.
- public static let SkipCanNotBecomeFirstResponderRow = RowNavigationOptions(.skipCanNotBecomeFirstResponderRow)
- }
- /**
- * Defines the configuration for the keyboardType of FieldRows.
- */
- public struct KeyboardReturnTypeConfiguration {
- /// Used when the next row is available.
- public var nextKeyboardType = UIReturnKeyType.next
- /// Used if next row is not available.
- public var defaultKeyboardType = UIReturnKeyType.default
- public init() {}
- public init(nextKeyboardType: UIReturnKeyType, defaultKeyboardType: UIReturnKeyType) {
- self.nextKeyboardType = nextKeyboardType
- self.defaultKeyboardType = defaultKeyboardType
- }
- }
- /**
- * Options that define when an inline row should collapse.
- */
- public struct InlineRowHideOptions: OptionSet {
- private enum _InlineRowHideOptions: Int {
- case never = 0, anotherInlineRowIsShown = 1, firstResponderChanges = 2
- }
- public let rawValue: Int
- public init(rawValue: Int) { self.rawValue = rawValue}
- private init(_ options: _InlineRowHideOptions ) { self.rawValue = options.rawValue }
- /// Never collapse automatically. Only when user taps inline row.
- public static let Never = InlineRowHideOptions(.never)
- /// Collapse qhen another inline row expands. Just one inline row will be expanded at a time.
- public static let AnotherInlineRowIsShown = InlineRowHideOptions(.anotherInlineRowIsShown)
- /// Collapse when first responder changes.
- public static let FirstResponderChanges = InlineRowHideOptions(.firstResponderChanges)
- }
- /// View controller that shows a form.
- open class FormViewController: UIViewController, FormViewControllerProtocol, FormDelegate {
- @IBOutlet public var tableView: UITableView!
- private lazy var _form: Form = { [weak self] in
- let form = Form()
- form.delegate = self
- return form
- }()
- public var form: Form {
- get { return _form }
- set {
- guard form !== newValue else { return }
- _form.delegate = nil
- tableView?.endEditing(false)
- _form = newValue
- _form.delegate = self
- if isViewLoaded {
- tableView?.reloadData()
- }
- }
- }
- /// Extra space to leave between between the row in focus and the keyboard
- open var rowKeyboardSpacing: CGFloat = 0
- /// Enables animated scrolling on row navigation
- open var animateScroll = false
- /// Accessory view that is responsible for the navigation between rows
- private var navigationAccessoryView: (UIView & NavigationAccessory)!
- /// Custom Accesory View to be used as a replacement
- open var customNavigationAccessoryView: (UIView & NavigationAccessory)? {
- return nil
- }
- /// Defines the behaviour of the navigation between rows
- public var navigationOptions: RowNavigationOptions?
- private var tableViewStyle: UITableView.Style = .grouped
- public init(style: UITableView.Style) {
- super.init(nibName: nil, bundle: nil)
- tableViewStyle = style
- }
- public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
- super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
- }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
- open override func viewDidLoad() {
- super.viewDidLoad()
- navigationAccessoryView = customNavigationAccessoryView ?? NavigationAccessoryView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 44.0))
- navigationAccessoryView.autoresizingMask = .flexibleWidth
- if tableView == nil {
- tableView = UITableView(frame: view.bounds, style: tableViewStyle)
- tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- tableView.cellLayoutMarginsFollowReadableWidth = false
- }
- if tableView.superview == nil {
- view.addSubview(tableView)
- }
- if tableView.delegate == nil {
- tableView.delegate = self
- }
- if tableView.dataSource == nil {
- tableView.dataSource = self
- }
- tableView.rowHeight = UITableView.automaticDimension
- tableView.estimatedRowHeight = BaseRow.estimatedRowHeight
- tableView.allowsSelectionDuringEditing = true
- }
- open override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- animateTableView = true
- let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? []
- if !selectedIndexPaths.isEmpty, tableView.window != nil {
- tableView.reloadRows(at: selectedIndexPaths, with: .none)
- }
- selectedIndexPaths.forEach {
- tableView.selectRow(at: $0, animated: false, scrollPosition: .none)
- }
- let deselectionAnimation = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in
- selectedIndexPaths.forEach {
- self?.tableView.deselectRow(at: $0, animated: context.isAnimated)
- }
- }
- let reselection = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in
- if context.isCancelled {
- selectedIndexPaths.forEach {
- self?.tableView.selectRow(at: $0, animated: false, scrollPosition: .none)
- }
- }
- }
- if let coordinator = transitionCoordinator {
- coordinator.animate(alongsideTransition: deselectionAnimation, completion: reselection)
- } else {
- selectedIndexPaths.forEach {
- tableView.deselectRow(at: $0, animated: false)
- }
- }
- NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
- if form.containsMultivaluedSection && (isBeingPresented || isMovingToParent) {
- tableView.setEditing(true, animated: false)
- }
- }
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
- }
- open override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
- super.prepare(for: segue, sender: sender)
- let baseRow = sender as? BaseRow
- baseRow?.prepare(for: segue)
- }
- /**
- Returns the navigation accessory view if it is enabled. Returns nil otherwise.
- */
- open func inputAccessoryView(for row: BaseRow) -> UIView? {
- let options = navigationOptions ?? Form.defaultNavigationOptions
- guard options.contains(.Enabled) else { return nil }
- guard row.baseCell.cellCanBecomeFirstResponder() else { return nil}
- navigationAccessoryView.previousEnabled = nextRow(for: row, withDirection: .up) != nil
- navigationAccessoryView.doneClosure = { [weak self] in
- self?.navigationDone()
- }
- navigationAccessoryView.previousClosure = { [weak self] in
- self?.navigationPrevious()
- }
- navigationAccessoryView.nextClosure = { [weak self] in
- self?.navigationNext()
- }
- navigationAccessoryView.nextEnabled = nextRow(for: row, withDirection: .down) != nil
- return navigationAccessoryView
- }
- // MARK: FormViewControllerProtocol
- /**
- Called when a cell becomes first responder
- */
- public final func beginEditing<T>(of cell: Cell<T>) {
- cell.row.isHighlighted = true
- cell.row.updateCell()
- RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row)
- cell.row.callbackOnCellHighlightChanged?()
- guard let _ = tableView, (form.inlineRowHideOptions ?? Form.defaultInlineRowHideOptions).contains(.FirstResponderChanges) else { return }
- let row = cell.baseRow
- let inlineRow = row?._inlineRow
- for row in form.allRows.filter({ $0 !== row && $0 !== inlineRow && $0._inlineRow != nil }) {
- if let inlineRow = row as? BaseInlineRowType {
- inlineRow.collapseInlineRow()
- }
- }
- }
- /**
- Called when a cell resigns first responder
- */
- public final func endEditing<T>(of cell: Cell<T>) {
- cell.row.isHighlighted = false
- cell.row.wasBlurred = true
- RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row)
- cell.row.callbackOnCellHighlightChanged?()
- if cell.row.validationOptions.contains(.validatesOnBlur) || (cell.row.wasChanged && cell.row.validationOptions.contains(.validatesOnChangeAfterBlurred)) {
- cell.row.validate()
- }
- cell.row.updateCell()
- }
- /**
- Returns the animation for the insertion of the given rows.
- */
- open func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation {
- return .fade
- }
- /**
- Returns the animation for the deletion of the given rows.
- */
- open func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation {
- return .fade
- }
- /**
- Returns the animation for the reloading of the given rows.
- */
- open func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation {
- return .automatic
- }
- /**
- Returns the animation for the insertion of the given sections.
- */
- open func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation {
- return .automatic
- }
- /**
- Returns the animation for the deletion of the given sections.
- */
- open func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation {
- return .automatic
- }
- /**
- Returns the animation for the reloading of the given sections.
- */
- open func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation {
- return .automatic
- }
- // MARK: TextField and TextView Delegate
- open func textInputShouldBeginEditing<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
- return true
- }
- open func textInputDidBeginEditing<T>(_ textInput: UITextInput, cell: Cell<T>) {
- if let row = cell.row as? KeyboardReturnHandler {
- let next = nextRow(for: cell.row, withDirection: .down)
- if let textField = textInput as? UITextField {
- textField.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ??
- (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) :
- (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ??
- Form.defaultKeyboardReturnType.defaultKeyboardType))
- } else if let textView = textInput as? UITextView {
- textView.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ??
- (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) :
- (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ??
- Form.defaultKeyboardReturnType.defaultKeyboardType))
- }
- }
- }
- open func textInputShouldEndEditing<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
- return true
- }
- open func textInputDidEndEditing<T>(_ textInput: UITextInput, cell: Cell<T>) {
- }
- open func textInput<T>(_ textInput: UITextInput, shouldChangeCharactersInRange range: NSRange, replacementString string: String, cell: Cell<T>) -> Bool {
- return true
- }
- open func textInputShouldClear<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
- return true
- }
- open func textInputShouldReturn<T>(_ textInput: UITextInput, cell: Cell<T>) -> Bool {
- if let nextRow = nextRow(for: cell.row, withDirection: .down) {
- if nextRow.baseCell.cellCanBecomeFirstResponder() {
- nextRow.baseCell.cellBecomeFirstResponder()
- return true
- }
- }
- tableView?.endEditing(true)
- return true
- }
- // MARK: FormDelegate
- open func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?) {}
- // MARK: UITableViewDelegate
- @objc open func tableView(_ tableView: UITableView, willBeginReorderingRowAtIndexPath indexPath: IndexPath) {
- // end editing if inline cell is first responder
- let row = form[indexPath]
- if let inlineRow = row as? BaseInlineRowType, row._inlineRow != nil {
- inlineRow.collapseInlineRow()
- }
- }
- // MARK: FormDelegate
- open func sectionsHaveBeenAdded(_ sections: [Section], at indexes: IndexSet) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.insertSections(indexes, with: insertAnimation(forSections: sections))
- tableView?.endUpdates()
- }
- open func sectionsHaveBeenRemoved(_ sections: [Section], at indexes: IndexSet) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.deleteSections(indexes, with: deleteAnimation(forSections: sections))
- tableView?.endUpdates()
- }
- open func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at indexes: IndexSet) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.reloadSections(indexes, with: reloadAnimation(oldSections: oldSections, newSections: newSections))
- tableView?.endUpdates()
- }
- open func rowsHaveBeenAdded(_ rows: [BaseRow], at indexes: [IndexPath]) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.insertRows(at: indexes, with: insertAnimation(forRows: rows))
- tableView?.endUpdates()
- }
- open func rowsHaveBeenRemoved(_ rows: [BaseRow], at indexes: [IndexPath]) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.deleteRows(at: indexes, with: deleteAnimation(forRows: rows))
- tableView?.endUpdates()
- }
- open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at indexes: [IndexPath]) {
- guard animateTableView else { return }
- tableView?.beginUpdates()
- tableView?.reloadRows(at: indexes, with: reloadAnimation(oldRows: oldRows, newRows: newRows))
- tableView?.endUpdates()
- }
- // MARK: Private
- var oldBottomInset: CGFloat?
- var animateTableView = false
- /** Calculates the height needed for a header or footer. */
- fileprivate func height(specifiedHeight: (() -> CGFloat)?, sectionView: UIView?, sectionTitle: String?) -> CGFloat {
- if let height = specifiedHeight {
- return height()
- }
- if let sectionView = sectionView {
- let height = sectionView.bounds.height
- if height == 0 {
- return UITableView.automaticDimension
- }
- return height
- }
- if let sectionTitle = sectionTitle,
- sectionTitle != "" {
- return UITableView.automaticDimension
- }
- // Fix for iOS 11+. By returning 0, we ensure that no section header or
- // footer is shown when self-sizing is enabled (i.e. when
- // tableView.estimatedSectionHeaderHeight or tableView.estimatedSectionFooterHeight
- // == UITableView.automaticDimension).
- if tableView.style == .plain {
- return 0
- }
- return UITableView.automaticDimension
- }
- }
- extension FormViewController : UITableViewDelegate {
- // MARK: UITableViewDelegate
- open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- return indexPath
- }
- open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- guard tableView == self.tableView else { return }
- let row = form[indexPath]
- // row.baseCell.cellBecomeFirstResponder() may be cause InlineRow collapsed then section count will be changed. Use orignal indexPath will out of section's bounds.
- if !row.baseCell.cellCanBecomeFirstResponder() || !row.baseCell.cellBecomeFirstResponder() {
- self.tableView?.endEditing(true)
- }
- row.didSelect()
- }
- open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- guard tableView == self.tableView else { return tableView.rowHeight }
- let row = form[indexPath.section][indexPath.row]
- return row.baseCell.height?() ?? tableView.rowHeight
- }
- open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
- guard tableView == self.tableView else { return tableView.estimatedRowHeight }
- let row = form[indexPath.section][indexPath.row]
- return row.baseCell.height?() ?? tableView.estimatedRowHeight
- }
- open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- return form[section].header?.viewForSection(form[section], type: .header)
- }
- open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
- return form[section].footer?.viewForSection(form[section], type:.footer)
- }
- open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
- return height(specifiedHeight: form[section].header?.height,
- sectionView: self.tableView(tableView, viewForHeaderInSection: section),
- sectionTitle: self.tableView(tableView, titleForHeaderInSection: section))
- }
- open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
- return height(specifiedHeight: form[section].footer?.height,
- sectionView: self.tableView(tableView, viewForFooterInSection: section),
- sectionTitle: self.tableView(tableView, titleForFooterInSection: section))
- }
- open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- let row = form[indexPath]
- guard !row.isDisabled else { return false }
- if row.trailingSwipe.actions.count > 0 { return true }
- if #available(iOS 11,*), row.leadingSwipe.actions.count > 0 { return true }
- guard let section = form[indexPath.section] as? BaseMultivaluedSection else { return false }
- guard !(indexPath.row == section.count - 1 && section.multivaluedOptions.contains(.Insert) && section.showInsertIconInAddButton) else {
- return true
- }
- if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil {
- return false
- }
- return true
- }
- open func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- if editingStyle == .delete {
- let row = form[indexPath]
- let section = row.section!
- if let _ = row.baseCell.findFirstResponder() {
- tableView.endEditing(true)
- }
- section.remove(at: indexPath.row)
- } else if editingStyle == .insert {
- guard var section = form[indexPath.section] as? BaseMultivaluedSection else { return }
- guard let multivaluedRowToInsertAt = section.multivaluedRowToInsertAt else {
- fatalError("Multivalued section multivaluedRowToInsertAt property must be set up")
- }
- let newRow = multivaluedRowToInsertAt(max(0, section.count - 1))
- section.insert(newRow, at: max(0, section.count - 1))
- DispatchQueue.main.async {
- tableView.isEditing = !tableView.isEditing
- tableView.isEditing = !tableView.isEditing
- }
- tableView.scrollToRow(at: IndexPath(row: section.count - 1, section: indexPath.section), at: .bottom, animated: true)
- if newRow.baseCell.cellCanBecomeFirstResponder() {
- newRow.baseCell.cellBecomeFirstResponder()
- } else if let inlineRow = newRow as? BaseInlineRowType {
- inlineRow.expandInlineRow()
- }
- }
- }
- open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
- guard let section = form[indexPath.section] as? BaseMultivaluedSection, section.multivaluedOptions.contains(.Reorder) && section.count > 1 else {
- return false
- }
- if section.multivaluedOptions.contains(.Insert) && (section.count <= 2 || indexPath.row == (section.count - 1)) {
- return false
- }
- if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil {
- return false
- }
- return true
- }
- open func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
- guard let section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return sourceIndexPath }
- guard sourceIndexPath.section == proposedDestinationIndexPath.section else { return sourceIndexPath }
- let destRow = form[proposedDestinationIndexPath]
- if destRow is BaseInlineRowType && destRow._inlineRow != nil {
- return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section)
- }
- if proposedDestinationIndexPath.row > 0 {
- let previousRow = form[IndexPath(row: proposedDestinationIndexPath.row - 1, section: proposedDestinationIndexPath.section)]
- if previousRow is BaseInlineRowType && previousRow._inlineRow != nil {
- return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section)
- }
- }
- if section.multivaluedOptions.contains(.Insert) && proposedDestinationIndexPath.row == section.count - 1 {
- return IndexPath(row: section.count - 2, section: sourceIndexPath.section)
- }
- return proposedDestinationIndexPath
- }
- open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
- guard var section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return }
- if sourceIndexPath.row < section.count && destinationIndexPath.row < section.count && sourceIndexPath.row != destinationIndexPath.row {
- let sourceRow = form[sourceIndexPath]
- animateTableView = false
- section.remove(at: sourceIndexPath.row)
- section.insert(sourceRow, at: destinationIndexPath.row)
- animateTableView = true
- // update the accessory view
- let _ = inputAccessoryView(for: sourceRow)
- }
- }
- open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
- guard let section = form[indexPath.section] as? BaseMultivaluedSection else {
- if form[indexPath].trailingSwipe.actions.count > 0 {
- return .delete
- }
- return .none
- }
- if section.multivaluedOptions.contains(.Insert) && indexPath.row == section.count - 1 {
- return section.showInsertIconInAddButton ? .insert : .none
- }
- if section.multivaluedOptions.contains(.Delete) {
- return .delete
- }
- return .none
- }
- open func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
- return self.tableView(tableView, editingStyleForRowAt: indexPath) != .none
- }
- @available(iOS 11,*)
- open func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
- guard !form[indexPath].leadingSwipe.actions.isEmpty else {
- return nil
- }
- return form[indexPath].leadingSwipe.contextualConfiguration
- }
- @available(iOS 11,*)
- open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
- guard !form[indexPath].trailingSwipe.actions.isEmpty else {
- return nil
- }
- return form[indexPath].trailingSwipe.contextualConfiguration
- }
- @available(iOS, deprecated: 13, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead")
- open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?{
- guard let actions = form[indexPath].trailingSwipe.contextualActions as? [UITableViewRowAction], !actions.isEmpty else {
- return nil
- }
- return actions
- }
- }
- extension FormViewController : UITableViewDataSource {
- // MARK: UITableViewDataSource
- open func numberOfSections(in tableView: UITableView) -> Int {
- return form.count
- }
- open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return form[section].count
- }
- open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- form[indexPath].updateCell()
- return form[indexPath].baseCell
- }
- open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- return form[section].header?.title
- }
- open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
- return form[section].footer?.title
- }
- open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
- return nil
- }
- open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
- return 0
- }
- }
- extension FormViewController : UIScrollViewDelegate {
- // MARK: UIScrollViewDelegate
- open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
- guard let tableView = tableView, scrollView === tableView else { return }
- tableView.endEditing(true)
- }
- }
- extension FormViewController {
- // MARK: KeyBoard Notifications
- /**
- Called when the keyboard will appear. Adjusts insets of the tableView and scrolls it if necessary.
- */
- @objc open func keyboardWillShow(_ notification: Notification) {
- guard let table = tableView, let cell = table.findFirstResponder()?.formCell() else { return }
- let keyBoardInfo = notification.userInfo!
- let endFrame = keyBoardInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue
- let keyBoardFrame = table.window!.convert(endFrame.cgRectValue, to: table.superview)
- var newBottomInset = table.frame.origin.y + table.frame.size.height - keyBoardFrame.origin.y + rowKeyboardSpacing
- if #available(iOS 11.0, *) {
- newBottomInset = newBottomInset - tableView.safeAreaInsets.bottom
- }
- var tableInsets = table.contentInset
- var scrollIndicatorInsets = table.scrollIndicatorInsets
- oldBottomInset = oldBottomInset ?? tableInsets.bottom
- if newBottomInset > oldBottomInset! {
- tableInsets.bottom = newBottomInset
- scrollIndicatorInsets.bottom = tableInsets.bottom
- UIView.beginAnimations(nil, context: nil)
- UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double))
- UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!)
- table.contentInset = tableInsets
- table.scrollIndicatorInsets = scrollIndicatorInsets
- if let selectedRow = table.indexPath(for: cell) {
- if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 11 {
- let rect = table.rectForRow(at: selectedRow)
- table.scrollRectToVisible(rect, animated: animateScroll)
- } else {
- table.scrollToRow(at: selectedRow, at: .none, animated: animateScroll)
- }
- }
- UIView.commitAnimations()
- }
- }
- /**
- Called when the keyboard will disappear. Adjusts insets of the tableView.
- */
- @objc open func keyboardWillHide(_ notification: Notification) {
- guard let table = tableView, let oldBottom = oldBottomInset else { return }
- let keyBoardInfo = notification.userInfo!
- var tableInsets = table.contentInset
- var scrollIndicatorInsets = table.scrollIndicatorInsets
- tableInsets.bottom = oldBottom
- scrollIndicatorInsets.bottom = tableInsets.bottom
- oldBottomInset = nil
- UIView.beginAnimations(nil, context: nil)
- UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double))
- UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!)
- table.contentInset = tableInsets
- table.scrollIndicatorInsets = scrollIndicatorInsets
- UIView.commitAnimations()
- }
- }
- public enum Direction { case up, down }
- extension FormViewController {
- // MARK: Navigation Methods
- @objc func navigationDone() {
- tableView?.endEditing(true)
- }
- @objc func navigationPrevious() {
- navigateTo(direction: .up)
- }
- @objc func navigationNext() {
- navigateTo(direction: .down)
- }
- public func navigateTo(direction: Direction) {
- guard let currentCell = tableView?.findFirstResponder()?.formCell() else { return }
- guard let currentIndexPath = tableView?.indexPath(for: currentCell) else { return }
- guard let nextRow = nextRow(for: form[currentIndexPath], withDirection: direction) else { return }
- if nextRow.baseCell.cellCanBecomeFirstResponder() {
- tableView?.scrollToRow(at: nextRow.indexPath!, at: .none, animated: animateScroll)
- nextRow.baseCell.cellBecomeFirstResponder(withDirection: direction)
- }
- }
- func nextRow(for currentRow: BaseRow, withDirection direction: Direction) -> BaseRow? {
- let options = navigationOptions ?? Form.defaultNavigationOptions
- guard options.contains(.Enabled) else { return nil }
- guard let next = direction == .down ? form.nextRow(for: currentRow) : form.previousRow(for: currentRow) else { return nil }
- if next.isDisabled && options.contains(.StopDisabledRow) {
- return nil
- }
- if !next.baseCell.cellCanBecomeFirstResponder() && !next.isDisabled && !options.contains(.SkipCanNotBecomeFirstResponderRow) {
- return nil
- }
- if !next.isDisabled && next.baseCell.cellCanBecomeFirstResponder() {
- return next
- }
- return nextRow(for: next, withDirection:direction)
- }
- }
- extension FormViewControllerProtocol {
- // MARK: Helpers
- func makeRowVisible(_ row: BaseRow, destinationScrollPosition: UITableView.ScrollPosition? = .bottom) {
- guard let destinationScrollPosition = destinationScrollPosition else { return }
- guard let cell = row.baseCell, let indexPath = row.indexPath, let tableView = tableView else { return }
- if cell.window == nil || (tableView.contentOffset.y + tableView.frame.size.height <= cell.frame.origin.y + cell.frame.size.height) {
- tableView.scrollToRow(at: indexPath, at: destinationScrollPosition, animated: true)
- }
- }
- }
|