// 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 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 { /** * 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 { /** * Shows the controller, created by the specified provider, with `showViewController(...)`. */ case show(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) /** * Presents the controller, created by the specified provider, modally. */ case presentModally(controllerProvider: ControllerProvider, 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, 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(of: Cell) func endEditing(of: Cell) 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(of cell: Cell) { 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(of cell: Cell) { 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(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputDidBeginEditing(_ textInput: UITextInput, cell: Cell) { 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(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputDidEndEditing(_ textInput: UITextInput, cell: Cell) { } open func textInput(_ textInput: UITextInput, shouldChangeCharactersInRange range: NSRange, replacementString string: String, cell: Cell) -> Bool { return true } open func textInputShouldClear(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> 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) } } }