├── .gitignore ├── LICENSE ├── README.md ├── TableSchemer.podspec ├── TableSchemer.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── TableSchemer.xcscheme │ └── TableSchemerExamples.xcscheme ├── TableSchemer ├── AccordionScheme.swift ├── AccordionSchemeBuilder.swift ├── ArrayScheme.swift ├── ArraySchemeBuilder.swift ├── AttributedScheme.swift ├── AttributedSchemeSet.swift ├── BasicScheme.swift ├── BasicSchemeBuilder.swift ├── InferrableReuseIdentifierScheme.swift ├── InferrableRowAnimatableScheme.swift ├── Info.plist ├── PrivacyInfo.xcprivacy ├── RadioScheme.swift ├── RadioSchemeBuilder.swift ├── Scheme.swift ├── SchemeBuilder.swift ├── SchemeCell.swift ├── SchemeRowAnimators.swift ├── SchemeSet.swift ├── SchemeSetBuilder.swift ├── StaticScheme.swift ├── StaticSchemeBuilder.swift ├── TableScheme.swift ├── TableSchemeBatchAnimator.swift ├── TableSchemeBuilder.swift ├── TableSchemer.h └── ViewExtensions.swift ├── TableSchemerExamples ├── AdvancedTableSchemeViewController.swift ├── AnimationsViewController.swift ├── AppDelegate.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist └── MasterViewController.swift └── TableSchemerTests ├── AccordionScheme_Tests.swift ├── ArrayScheme_Tests.swift ├── BasicScheme_Tests.swift ├── Info.plist ├── RadioScheme_Tests.swift ├── SchemeSetBuilder_Tests.swift ├── SchemeSet_Tests.swift ├── TableScheme_Tests.swift └── TableSchemerTests-Bridging-Header.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcbkptlist 2 | *.xcuserstate 3 | Pods 4 | .DS_Store 5 | *.xcuserdatad 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, Weebly All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Weebly nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Weebly, Inc BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TableSchemer is a framework for building static interactive table views. Interface Builder provides a great way to build out static table views, but not everyone uses interface builder, and adding interactivity to these table views is difficult. Writing interactive static table views traditionally is a tiresome task due to working with index paths, and having multiple delegate methods to handle configuration, sizing, and selection handling. They're also a huge pain to maintain when the need to reorder them comes as you need to update the index paths in all those locations. 2 | 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | 5 | ## Features 6 | 7 | ### Closure-based Table Views 8 | 9 | Build your table view using closures, and have all of your cell(s) logic in one place. Forget index path comparisons, and focus on writing your logic. Thanks to Generics in Swift you can just work with your cell type and not worry about casting. 10 | 11 | ### Built-in Schemes 12 | 13 | TableSchemer comes with a variety of powerful schemes already built-in. Included is a basic scheme (for when you just need to render one cell), a radio scheme (for when you need a single cell in a group of cells selected at once), an array scheme (for when you need a dynamic set of cells backed by specific objects), and an accordion scheme (for when you need one cell to expand into many for a collapsed selection). Check out how to use them [here](https://github.com/Weebly/TableSchemer/wiki/Built-in-Schemes). 14 | 15 | ### Extendable 16 | 17 | You can easily create your own Schemes and add them into your tables. With a few method overrides you can start creating unique, intuitive controls for your users. 18 | 19 | ## Getting Started 20 | 21 | * Download TableSchemer and see it in action using the example app. See [Sample Project](#sample-project) for instructions on how to get the sample project running. 22 | * Start building your own tables by installing it in your own app. See [Using Table Schemer](https://github.com/Weebly/TableSchemer/wiki/Using-Table-Schemer) for more information on how to use Table Schemer. 23 | 24 | ## Requirements 25 | 26 | TableSchemer is built using Swift 5, so it requires that you use Xcode 10.2. It supports iOS 8.0+. 27 | 28 | ## Usage 29 | 30 | TableSchemer works by creating a TableScheme object and setting it as your UITableView's dataSource property. Here's an example of using it with a UITableViewController: 31 | 32 | ```swift 33 | class MasterViewController: UITableViewController { 34 | 35 | var tableScheme: TableScheme! 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | createTableScheme() 41 | tableView.rowHeight = 44.0 42 | } 43 | 44 | func createTableScheme() { 45 | tableScheme = TableScheme(tableView: tableView) { builder in 46 | builder.buildSchemeSet { builder in 47 | builder.buildScheme { (scheme: BasicSchemeBuilder) in 48 | 49 | scheme.configurationHandler = { cell in 50 | cell.textLabel?.text = "Tap here for an advanced example." 51 | cell.accessoryType = .disclosureIndicator 52 | } 53 | 54 | // We're specifying weak self here because handlers are retained by the schemes. Without it, we'd have a retain cycle. 55 | scheme.selectionHandler = { [weak self] cell, scheme in 56 | let advancedController = AdvancedTableSchemeViewController(style: .grouped) 57 | self?.navigationController?.pushViewController(advancedController, animated: true) 58 | } 59 | 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | ``` 67 | 68 | TableSchemer will set itself as the data source and delegate of the table view. If you need to be the delegate to the table view update it after creating the scheme. To use the built-in selection and height handling you need to be sure to forward those delegate methods to the tableScheme object. The signatures are the same as in `UITableViewDelegate`. Check out the [Using Table Schemer](https://github.com/Weebly/TableSchemer/wiki/Using-Table-Schemer) page for more, and be sure to check out our sample app! 69 | 70 | ## Sample Project 71 | 72 | There is a sample project that demonstrates a number of ways to make use of TableSchemer. To run them, clone the repo and run the TableSchemerExamples target. 73 | 74 | ## Contact 75 | 76 | * If you need help or have a general question use [Stack Overflow](https://stackoverflow.com/questions/tagged/tableschemer) 77 | * If you've found a bug or have a feature request [open an issue](https://github.com/weebly/TableSchemer/issues/new) 78 | 79 | We're also frequently in the [Gitter](https://gitter.im/weebly/TableSchemer) chatroom! 80 | 81 | ## Contributing 82 | 83 | We love to have your help to make TableSchemer better. Feel free to 84 | 85 | - open an issue if you run into any problem. 86 | - fork the project and submit pull request. Before the pull requests can be accepted, a Contributors Licensing Agreement must be signed. 87 | 88 | ## License 89 | 90 | Copyright (c) 2019, Square 91 | 92 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 93 | 94 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Weebly nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Weebly, Inc BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 95 | -------------------------------------------------------------------------------- /TableSchemer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TableSchemer" 3 | s.version = "4.0.3" 4 | s.summary = "Interactive static table views with ease" 5 | 6 | s.description = <<-DESC 7 | TableSchemer is a framework for building static interactive table views. Interface Builder provides a great way to build out static table views, but not everyone uses interface builder, and adding interactivity to these table views is difficult. Writing interactive static table views traditionally is a tiresome task due to working with index paths, and having multiple delegate methods to handle configuration, sizing, and selection handling. They're also a huge pain to maintain when the need to reorder them comes as you need to update the index paths in all those locations. 8 | DESC 9 | 10 | s.homepage = "https://github.com/Weebly/TableSchemer" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "Jace Conflenti" => "jace@squareup.com" } 13 | s.social_media_url = "http://twitter.com/ketzusaka" 14 | s.platform = :ios, "12.0" 15 | s.source = { :git => "https://github.com/Weebly/TableSchemer.git", :tag => "v4.0.3" } 16 | s.source_files = "TableSchemer/*.swift" 17 | s.resource_bundles = {'TableSchemer_privacy' => ['TableSchemer/PrivacyInfo.xcprivacy']} 18 | s.requires_arc = true 19 | s.swift_version = "5.0" 20 | end 21 | -------------------------------------------------------------------------------- /TableSchemer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TableSchemer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TableSchemer.xcodeproj/xcshareddata/xcschemes/TableSchemer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /TableSchemer.xcodeproj/xcshareddata/xcschemes/TableSchemerExamples.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /TableSchemer/AccordionScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccordionScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/12/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class AccordionScheme: BasicScheme { 12 | 13 | public typealias AccordionConfigurationHandler = (_ cell: ExpandedCellType, _ index: Int) -> Void 14 | public typealias AccordionSelectionHandler = (_ cell: ExpandedCellType, _ scheme: AccordionScheme, _ selectedIndex: Int) -> Void 15 | 16 | open var expandedCellTypes: [UITableViewCell.Type] 17 | 18 | /** The height used for each accordion cell if asked. */ 19 | open var accordionHeights: [RowHeight]? 20 | 21 | /** The currently selected index. */ 22 | open var selectedIndex = 0 23 | 24 | /** The closure called to handle accordion cells when the accordion is expanded. */ 25 | open var accordionConfigurationHandler: AccordionConfigurationHandler 26 | 27 | /** 28 | The closure called when an accordion cell is selected. 29 | 30 | NOTE: This is only called if the TableScheme is asked to handle selection by the table view delegate. 31 | */ 32 | open var accordionSelectionHandler: AccordionSelectionHandler? 33 | 34 | /** Whether the accordion is expanded or not. */ 35 | open var expanded = false 36 | 37 | public init(expandedCellTypes: [UITableViewCell.Type], collapsedCellConfigurationHandler: @escaping ConfigurationHandler, expandedCellConfigurationHandler: @escaping AccordionConfigurationHandler) { 38 | accordionConfigurationHandler = expandedCellConfigurationHandler 39 | self.expandedCellTypes = expandedCellTypes 40 | super.init(configurationHandler: collapsedCellConfigurationHandler) 41 | } 42 | 43 | // MARK: Property Overrides 44 | open override var numberOfCells: Int { 45 | return expanded ? numberOfItems : 1 46 | } 47 | 48 | public var numberOfItems: Int { 49 | return expandedCellTypes.count 50 | } 51 | 52 | // MARK: Public Instance Methods 53 | override open func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) { 54 | if expanded { 55 | accordionConfigurationHandler(cell as! ExpandedCellType, relativeIndex) 56 | } else { 57 | super.configureCell(cell, withRelativeIndex: relativeIndex) 58 | } 59 | } 60 | 61 | override open func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) { 62 | var prependedIndexPaths = Array() 63 | var appendedIndexPaths = Array() 64 | 65 | tableView.beginUpdates() 66 | 67 | if expanded { 68 | if let ash = accordionSelectionHandler { 69 | ash(cell as! ExpandedCellType, self, relativeIndex) 70 | } 71 | 72 | selectedIndex = relativeIndex 73 | 74 | for i in 0.. 0 { 85 | tableView.deleteRows(at: prependedIndexPaths, with: .fade) 86 | } 87 | 88 | if appendedIndexPaths.count > 0 { 89 | tableView.deleteRows(at: appendedIndexPaths, with: .fade) 90 | } 91 | } else { 92 | super.selectCell(cell, inTableView: tableView, inSection: section, havingRowsBeforeScheme: rowsBeforeScheme, withRelativeIndex: relativeIndex) 93 | 94 | for i in 0.. 0 { 105 | tableView.insertRows(at: prependedIndexPaths, with: .fade) 106 | } 107 | 108 | if appendedIndexPaths.count > 0 { 109 | tableView.insertRows(at: appendedIndexPaths, with: .fade) 110 | } 111 | } 112 | 113 | let reloadRow = IndexPath(row: rowsBeforeScheme + relativeIndex, section: section) 114 | tableView.reloadRows(at: [reloadRow], with: .automatic) 115 | 116 | expanded = !expanded 117 | 118 | tableView.endUpdates() 119 | } 120 | 121 | override open func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String { 122 | if expanded { 123 | return String(describing: expandedCellTypes[relativeIndex]) 124 | } else { 125 | return super.reuseIdentifier(forRelativeIndex: relativeIndex) 126 | } 127 | } 128 | 129 | override open func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 130 | if expanded { 131 | if accordionHeights != nil && accordionHeights!.count > relativeIndex { 132 | return accordionHeights![relativeIndex] 133 | } else { 134 | return .useTable 135 | } 136 | } else { 137 | return super.height(forRelativeIndex: relativeIndex) 138 | } 139 | } 140 | 141 | override open var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { 142 | return [(identifier: String(describing: CollapsedCellType.self), cellType: CollapsedCellType.self)] + expandedCellTypes.map { (identifier: String(describing: $0), cellType: $0) } 143 | } 144 | 145 | } 146 | 147 | extension AccordionScheme: InferrableRowAnimatableScheme { 148 | 149 | public typealias IdentifierType = String 150 | 151 | public var rowIdentifiers: [IdentifierType] { 152 | return expanded ? expandedCellTypes.map(String.init(describing:)) : [super.reuseIdentifier(forRelativeIndex: 0)] 153 | } 154 | 155 | } 156 | 157 | -------------------------------------------------------------------------------- /TableSchemer/AccordionSchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccordionSchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/25/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | open class AccordionSchemeBuilder: SchemeBuilder { 10 | public typealias SchemeType = AccordionScheme 11 | 12 | public required init() {} 13 | 14 | public func createScheme() throws -> SchemeType { 15 | 16 | 17 | guard let collapsedConfigurationHandler = collapsedCellConfigurationHandler else { 18 | throw SchemeBuilderError.missingRequiredAttribute("collapsedCellConfigurationHandler") 19 | } 20 | 21 | guard let expandedConfigurationHandler = expandedCellConfigurationHandler else { 22 | throw SchemeBuilderError.missingRequiredAttribute("expandedCellConfigurationHandler") 23 | } 24 | 25 | guard let expandedCellTypes = expandedCellTypes else { 26 | throw SchemeBuilderError.missingRequiredAttribute("expandedCellTypes") 27 | } 28 | 29 | let scheme = AccordionScheme(expandedCellTypes: expandedCellTypes, collapsedCellConfigurationHandler: collapsedConfigurationHandler, expandedCellConfigurationHandler: expandedConfigurationHandler) 30 | scheme.expanded = expanded 31 | scheme.height = height 32 | scheme.selectionHandler = collapsedCellSelectionHandler 33 | scheme.accordionSelectionHandler = expandedCellSelectionHandler 34 | scheme.selectedIndex = selectedIndex 35 | scheme.accordionHeights = accordionHeights 36 | 37 | return scheme 38 | } 39 | 40 | open var expandedCellTypes: [UITableViewCell.Type]? 41 | open var accordionHeights: [RowHeight]? 42 | open var selectedIndex = 0 43 | open var expanded = false 44 | open var height: RowHeight = .useTable 45 | open var collapsedCellConfigurationHandler: SchemeType.ConfigurationHandler? 46 | open var collapsedCellSelectionHandler: SchemeType.SelectionHandler? 47 | open var expandedCellConfigurationHandler: SchemeType.AccordionConfigurationHandler! 48 | open var expandedCellSelectionHandler: SchemeType.AccordionSelectionHandler? 49 | 50 | } 51 | -------------------------------------------------------------------------------- /TableSchemer/ArrayScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/12/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** This class is used with `TableScheme` to display an array of cells. 12 | 13 | Use this scheme when you want to have a set of cells that are based on an `Array`. An example of this case 14 | is displaying a font to choose, or wifi networks. 15 | 16 | It's recommended that you don't create these directly, and let the 17 | `SchemeSetBuilder.buildScheme(handler:)` method generate them 18 | for you. 19 | */ 20 | open class ArrayScheme: Scheme { 21 | 22 | public typealias ConfigurationHandler = (_ cell: CellType, _ object: ElementType) -> Void 23 | public typealias SelectionHandler = (_ cell: CellType, _ scheme: ArrayScheme, _ object: ElementType) -> Void 24 | public typealias HeightHandler = (_ object: ElementType) -> RowHeight 25 | public typealias ReorderingHandler = (_ objects: [ElementType]) -> Void 26 | 27 | /** The objects this scheme is representing */ 28 | open var objects: [ElementType] 29 | 30 | /** 31 | The closure called to determine the height of this cell. 32 | 33 | Unlike other `Scheme` implementations that take predefined 34 | values this scheme uses a closure because the height may change 35 | due to the underlying objects state, and this felt like a better 36 | API to accomodate that. 37 | 38 | This closure is only used if the table view delegate asks its 39 | `TableScheme` for the height with ` height(tableView:forIndexPath:) 40 | */ 41 | open var heightHandler: HeightHandler? 42 | 43 | /** The closure called for configuring the cell the scheme is representing. */ 44 | open var configurationHandler: ConfigurationHandler 45 | 46 | /** 47 | The closure called when the cell is selected. 48 | 49 | NOTE: This is only called if the TableScheme is asked to handle selection 50 | by the table view delegate. 51 | */ 52 | open var selectionHandler: SelectionHandler? 53 | 54 | /** 55 | The closure called when objects have been reordered by a drag-and-drop operation. 56 | 57 | If the value is `nil` the cells will not be reorderable. 58 | */ 59 | open var reorderingHandler: ReorderingHandler? 60 | 61 | // MARK: Property Overrides 62 | open var numberOfCells: Int { 63 | return objects.count 64 | } 65 | 66 | public init(objects: [ElementType], configurationHandler: @escaping ConfigurationHandler) { 67 | self.objects = objects 68 | self.configurationHandler = configurationHandler 69 | } 70 | 71 | // MARK: Public Instance Methods 72 | open func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) { 73 | configurationHandler(cell as! CellType, objects[relativeIndex]) 74 | } 75 | 76 | open func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) { 77 | if let sh = selectionHandler { 78 | sh(cell as! CellType, self, objects[relativeIndex]) 79 | } 80 | } 81 | 82 | open func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String { 83 | return String(describing: CellType.self) 84 | } 85 | 86 | open func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 87 | if let hh = heightHandler { 88 | return hh(objects[relativeIndex]) 89 | } else { 90 | return .useTable 91 | } 92 | } 93 | 94 | @available(iOS 11.0, *) 95 | public func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { 96 | guard reorderingHandler != nil else { return [] } 97 | let dragItem = UIDragItem(itemProvider: NSItemProvider()) 98 | dragItem.localObject = objects[indexPath.row] 99 | return [dragItem] 100 | } 101 | 102 | @available(iOS 11.0, *) 103 | public func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { 104 | guard reorderingHandler != nil, session.localDragSession != nil else { 105 | return UITableViewDropProposal(operation: .cancel) 106 | } 107 | return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) 108 | } 109 | 110 | @available(iOS 11.0, *) 111 | public func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { 112 | 113 | guard let reorderingHandler = reorderingHandler, 114 | coordinator.proposal.operation == .move, 115 | coordinator.items.count == 1, 116 | let item = coordinator.items.first, 117 | let sourceIndexPath = item.sourceIndexPath, 118 | let localObject = item.dragItem.localObject as? ElementType else { 119 | return 120 | } 121 | 122 | let destinationIndexPath: IndexPath 123 | 124 | if let indexPath = coordinator.destinationIndexPath { 125 | destinationIndexPath = indexPath 126 | } else { 127 | // Get last index path of table view. 128 | let section = tableView.numberOfSections - 1 129 | let row = tableView.numberOfRows(inSection: section) 130 | destinationIndexPath = IndexPath(row: row, section: section) 131 | } 132 | 133 | tableView.performBatchUpdates({ 134 | objects.remove(at: sourceIndexPath.row) 135 | objects.insert(localObject, at: destinationIndexPath.row) 136 | 137 | tableView.insertRows(at: [destinationIndexPath], with: .none) 138 | tableView.deleteRows(at: [sourceIndexPath], with: .fade) 139 | 140 | }, completion: { _ in 141 | reorderingHandler(self.objects) 142 | }) 143 | } 144 | 145 | } 146 | 147 | extension ArrayScheme: InferrableRowAnimatableScheme { 148 | 149 | public typealias IdentifierType = ElementType 150 | 151 | public var rowIdentifiers: [IdentifierType] { 152 | return objects 153 | } 154 | 155 | } 156 | 157 | extension ArrayScheme: InferrableReuseIdentifierScheme { 158 | 159 | public var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { 160 | return [(identifier: String(describing: CellType.self), cellType: CellType.self)] 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /TableSchemer/ArraySchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArraySchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/25/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | open class ArraySchemeBuilder: SchemeBuilder { 10 | 11 | public typealias SchemeType = ArrayScheme 12 | 13 | public required init() {} 14 | 15 | public func createScheme() throws -> SchemeType { 16 | guard let objects = objects else { 17 | throw SchemeBuilderError.missingRequiredAttribute("objects") 18 | } 19 | 20 | guard let configurationHandler = configurationHandler else { 21 | throw SchemeBuilderError.missingRequiredAttribute("configurationHandler") 22 | } 23 | 24 | let scheme = SchemeType(objects: objects, configurationHandler: configurationHandler) 25 | scheme.selectionHandler = selectionHandler 26 | scheme.heightHandler = heightHandler 27 | scheme.reorderingHandler = reorderingHandler 28 | 29 | return scheme 30 | } 31 | 32 | open var objects: [ElementType]? 33 | open var heightHandler: SchemeType.HeightHandler? 34 | open var configurationHandler: SchemeType.ConfigurationHandler? 35 | open var selectionHandler: SchemeType.SelectionHandler? 36 | open var reorderingHandler: SchemeType.ReorderingHandler? 37 | 38 | } 39 | -------------------------------------------------------------------------------- /TableSchemer/AttributedScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/24/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | /** 10 | Each `Scheme` contained in a `SchemeSet` requires additional attributes. This 11 | struct contains the `Scheme` and its attributes. 12 | */ 13 | public struct AttributedScheme { 14 | 15 | public let scheme: Scheme 16 | public var hidden: Bool 17 | 18 | public init(scheme: Scheme, hidden: Bool) { 19 | self.scheme = scheme 20 | self.hidden = hidden 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /TableSchemer/AttributedSchemeSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedSchemeSet.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/24/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | /** 10 | Each `SchemeSet` contained in a `TableScheme` requires additional attributes. This 11 | struct contains the `SchemeSet` and its attributes. 12 | */ 13 | public struct AttributedSchemeSet { 14 | 15 | public let schemeSet: SchemeSet 16 | public var hidden: Bool 17 | 18 | public init(schemeSet: SchemeSet, hidden: Bool) { 19 | self.schemeSet = schemeSet 20 | self.hidden = hidden 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /TableSchemer/BasicScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/12/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** This class is used with a `TableScheme` as a single cell. 12 | 13 | Use this scheme when you want to have a single cell. 14 | 15 | It's recommended that you don't create these directly, and let the 16 | `SchemeSetBuilder.buildScheme(handler:)` method generate them 17 | for you. 18 | */ 19 | open class BasicScheme: Scheme, InferrableReuseIdentifierScheme { 20 | 21 | public typealias ConfigurationHandler = (_ cell: CellType) -> Void 22 | public typealias SelectionHandler = (_ cell: CellType, _ scheme: BasicScheme) -> Void 23 | 24 | /** The height the cell should be if asked. */ 25 | open var height: RowHeight = .useTable 26 | 27 | /** The closure called to configure the cell the scheme is representing. */ 28 | open var configurationHandler: ConfigurationHandler 29 | 30 | /** 31 | The closure called when the cell is selected. 32 | 33 | NOTE: This is only called if the TableScheme is asked to handle selection 34 | by the table view delegate. 35 | */ 36 | open var selectionHandler: SelectionHandler? 37 | 38 | open var numberOfCells: Int { return 1 } 39 | 40 | public init(configurationHandler: @escaping ConfigurationHandler) { 41 | self.configurationHandler = configurationHandler 42 | } 43 | 44 | // MARK: Public Instance Methods 45 | open func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) { 46 | configurationHandler(cell as! CellType) 47 | } 48 | 49 | open func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) { 50 | if let sh = selectionHandler { 51 | sh(cell as! CellType, self) 52 | } 53 | } 54 | 55 | open func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String { 56 | return String(describing: CellType.self) 57 | } 58 | 59 | open func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 60 | return height 61 | } 62 | 63 | open var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { 64 | return [(identifier: String(describing: CellType.self), cellType: CellType.self)] 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /TableSchemer/BasicSchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicSchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/25/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | open class BasicSchemeBuilder: SchemeBuilder { 10 | 11 | public typealias SchemeType = BasicScheme 12 | 13 | public required init() {} 14 | 15 | public func createScheme() throws -> SchemeType { 16 | guard let configurationHandler = configurationHandler else { 17 | throw SchemeBuilderError.missingRequiredAttribute("configurationHandler") 18 | } 19 | 20 | let scheme = BasicScheme(configurationHandler: configurationHandler) 21 | scheme.height = height 22 | scheme.selectionHandler = selectionHandler 23 | return scheme 24 | } 25 | 26 | open var height: RowHeight = .useTable 27 | open var configurationHandler: SchemeType.ConfigurationHandler? 28 | open var selectionHandler: SchemeType.SelectionHandler? 29 | 30 | } 31 | -------------------------------------------------------------------------------- /TableSchemer/InferrableReuseIdentifierScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InferrableReuseIdentifierScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/24/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | public protocol InferrableReuseIdentifierScheme: Scheme { 10 | var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { get } 11 | } 12 | -------------------------------------------------------------------------------- /TableSchemer/InferrableRowAnimatableScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InferrableRowAnimatableScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/17/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | public protocol InferrableRowAnimatableScheme { 10 | associatedtype IdentifierType: Equatable 11 | var rowIdentifiers: [IdentifierType] { get } 12 | } 13 | -------------------------------------------------------------------------------- /TableSchemer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TableSchemer/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TableSchemer/RadioScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** This class is used with a `TableScheme` to display a radio group of cells. 12 | 13 | Use this scheme when you want to have a set of cells that represent 14 | a single selection, similar to a radio group would in HTML. 15 | 16 | In order for this scheme to handle changing selection be sure that your 17 | table view delegate calls `TableScheme.handleSelectionInTableView(tableView:forIndexPath:)`. 18 | 19 | It's recommended that you don't create these directly, and let the 20 | `SchemeSetBuilder.buildScheme(handler:)` method generate them 21 | for you. 22 | */ 23 | open class RadioScheme: Scheme { 24 | 25 | public typealias ConfigurationHandler = (_ cell: CellType, _ index: Int) -> Void 26 | public typealias SelectionHandler = (_ cell: CellType, _ scheme: RadioScheme, _ index: Int) -> Void 27 | public typealias StateHandler = (_ cell: CellType, _ scheme: RadioScheme, _ index: Int, _ selected: Bool) -> Void 28 | 29 | /** The currently selected index. */ 30 | open var selectedIndex = 0 31 | 32 | /** The reuse identifiers that each cell will use. */ 33 | open var expandedCellTypes: [UITableViewCell.Type] 34 | 35 | /** The heights that the cells should have if asked. */ 36 | open var heights: [RowHeight]? 37 | 38 | /** The closure called for configuring the cell the scheme is representing. */ 39 | open var configurationHandler: ConfigurationHandler 40 | 41 | /** 42 | The closure called when the cell is selected. 43 | 44 | NOTE: This is only called if the TableScheme is asked to handle selection 45 | by the table view delegate. 46 | */ 47 | open var selectionHandler: SelectionHandler? 48 | 49 | /** 50 | The closure called when a cells selection state should be updated. By 51 | default, this will update the accessory type to `.checkmark` for the selected 52 | cell, and `.none` for a deselected cell. 53 | 54 | Do not use this closure as a way to handle selection; assign a selectionHandler 55 | instead as this closure is called during configuration as well. 56 | */ 57 | open var stateHandler: StateHandler = { cell, _, _, selected in 58 | cell.accessoryType = selected ? .checkmark : .none 59 | } 60 | 61 | public init(expandedCellTypes: [UITableViewCell.Type], configurationHandler: @escaping ConfigurationHandler) { 62 | self.expandedCellTypes = expandedCellTypes 63 | self.configurationHandler = configurationHandler 64 | } 65 | 66 | // MARK: Property Overrides 67 | open var numberOfCells: Int { 68 | return reusePairs.count 69 | } 70 | 71 | // MARK: Public Instance Methods 72 | 73 | open func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) { 74 | configurationHandler(cell as! CellType, relativeIndex) 75 | stateHandler(cell as! CellType, self, relativeIndex, selectedIndex == relativeIndex) 76 | } 77 | 78 | open func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) { 79 | if let sh = selectionHandler { 80 | sh(cell as! CellType, self, relativeIndex) 81 | } 82 | 83 | let oldSelectedIndex = selectedIndex 84 | 85 | if relativeIndex == oldSelectedIndex { 86 | return 87 | } 88 | 89 | selectedIndex = relativeIndex 90 | 91 | if let previouslySelectedCell = tableView.cellForRow(at: IndexPath(row: rowsBeforeScheme + oldSelectedIndex, section: section)) { 92 | stateHandler(previouslySelectedCell as! CellType, self, oldSelectedIndex, false) 93 | } 94 | 95 | stateHandler(cell as! CellType, self, relativeIndex, true) 96 | } 97 | 98 | open func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String { 99 | return String(describing: expandedCellTypes[relativeIndex]) 100 | } 101 | 102 | open func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 103 | var height = RowHeight.useTable 104 | 105 | if let rowHeights = heights , rowHeights.count > relativeIndex { 106 | height = rowHeights[relativeIndex] 107 | } 108 | 109 | return height 110 | } 111 | 112 | } 113 | 114 | extension RadioScheme: InferrableReuseIdentifierScheme { 115 | 116 | public var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { 117 | return expandedCellTypes.map { (identifier: String(describing: $0), cellType: $0) } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /TableSchemer/RadioSchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioSchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/25/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | open class RadioSchemeBuilder: SchemeBuilder { 10 | 11 | public typealias SchemeType = RadioScheme 12 | 13 | public required init() {} 14 | 15 | open var configurationHandler: SchemeType.ConfigurationHandler? 16 | open var selectionHandler: SchemeType.SelectionHandler? 17 | open var stateHandler: SchemeType.StateHandler? 18 | open var expandedCellTypes: [UITableViewCell.Type]? 19 | open var selectedIndex = 0 20 | open var heights: [RowHeight]? 21 | 22 | public func createScheme() throws -> SchemeType { 23 | guard let configurationHandler = configurationHandler else { 24 | throw SchemeBuilderError.missingRequiredAttribute("configurationHandler") 25 | } 26 | 27 | guard let expandedCellTypes = expandedCellTypes else { 28 | throw SchemeBuilderError.missingRequiredAttribute("expandedCellTypes") 29 | } 30 | 31 | let scheme = SchemeType(expandedCellTypes: expandedCellTypes, configurationHandler: configurationHandler) 32 | scheme.heights = heights 33 | scheme.selectedIndex = selectedIndex 34 | scheme.selectionHandler = selectionHandler 35 | 36 | if let stateHandler = stateHandler { 37 | scheme.stateHandler = stateHandler 38 | } 39 | 40 | return scheme 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /TableSchemer/Scheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/12/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum RowHeight: Equatable { 12 | case useTable 13 | case custom(CGFloat) 14 | } 15 | 16 | public func ==(lhs: RowHeight, rhs: RowHeight) -> Bool { 17 | switch lhs { 18 | case .useTable: 19 | switch rhs { 20 | case .useTable: 21 | return true 22 | case .custom(_): 23 | return false 24 | } 25 | case .custom(let lhsHeight): 26 | switch rhs { 27 | case .useTable: 28 | return false 29 | case .custom(let rhsHeight): 30 | return lhsHeight == rhsHeight 31 | } 32 | } 33 | } 34 | 35 | /** A Scheme defines one or many rows of a static table view, depending on the type of scheme. 36 | 37 | This protocol can be used as a type of placeholder object for `UITableViewCell`'s. It's used in 38 | conjunction with a `TableScheme`, which takes an array of `SchemeSet` objects and for each 39 | row will ask the `Scheme` for its reuseIdentifier to dequeue a cell, and then call the 40 | configurationHandler to allow setup of the cell based on the scheme of the cell. 41 | */ 42 | public protocol Scheme: AnyObject { 43 | /** 44 | This property determines how many cells should be represented by this `Scheme`. 45 | This is used to determine the size of the table view. 46 | */ 47 | var numberOfCells: Int { get } 48 | 49 | /** 50 | This method is called by `TableScheme` when the cell needs 51 | to be configured. It will be passed the cell being created and 52 | the relative index the cell is from the start of the scheme. For 53 | example, if your scheme has 3 different cells, this will be called 54 | three times with relativeIndexes 0, 1 and 2. 55 | 56 | - parameter cell: The UITableViewCell being created. 57 | - parameter relativeIndex: The cell index from the start of the scheme being configured. 58 | */ 59 | func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) 60 | 61 | /** 62 | This method is called by a `TableScheme` when the cell 63 | is selected, providing the delegate asks the data source 64 | to handle the selection. This is passed the `UITableView`, section, 65 | and rowsBeforeScheme so that the selection handler may alter 66 | the table views state if needed. An example of a scheme that 67 | needs this functionality is the accordion scheme. 68 | 69 | - parameter cell: The UITableViewCell that was selected. 70 | - parameter tableView: The UITableView the cell belongs to. 71 | - parameter section: The section that the cell belongs to. 72 | - parameter rowsBeforeScheme: The number of rows before the scheme's first cell. 73 | - parameter relativeIndex: The index of the row from the scheme's first cell. 74 | */ 75 | func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) 76 | 77 | /** 78 | This method is called by `TableScheme` when the cell 79 | is being configured to determine which reuseIdentifier should 80 | be used to query the table view. 81 | 82 | - parameter relativeIndex: The index of the row from the schemes first cell. 83 | - returns: The reuse identifier to pass into the table views dequeue method. 84 | */ 85 | func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String 86 | 87 | /** 88 | This method is called by `TableScheme` when the cell's 89 | height is requested, providing the delegate asks the data source 90 | to handle the height. It should be overriden by subclasses. 91 | 92 | - parameter relativeIndex: The index of the row from the scheme's first cell. 93 | - returns: The height to be used for the cell. Use TableHeight.UseTable to use the 94 | tableView's height, otherwise provide TableHeight.Custom(CGFloat) to use 95 | a custom height. 96 | */ 97 | func height(forRelativeIndex relativeIndex: Int) -> RowHeight 98 | 99 | /** 100 | This method is called by `TableScheme` when we want to generate 101 | a cell. This method has a default implementation that should generally 102 | be used, but in some situations it is desirable to manage cell creation 103 | yourself. 104 | 105 | - parameter tableView: The `UITableView` that contains the created `UITableViewCell` 106 | - parameter indexPath: The `IndexPath` that the cell belongs at 107 | - parameter relativeIndex: The relative index this cell has within the `Scheme`s cells 108 | - returns: The `UITableViewCell` to be used in the `UITableView` 109 | */ 110 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, relativeIndex: Int) -> UITableViewCell 111 | 112 | /** 113 | This method is called by `TableScheme` when we want to start a drag. 114 | 115 | - parameter tableView: The `UITableView` that contains the dragged cell 116 | - parameter session: The `UIDragSession` that will hold the dragged item 117 | - parameter indexPath: The index path of the dragged cell 118 | - returns: An array of `UIDragItem` which represents the dragged row 119 | */ 120 | @available(iOS 11.0, *) 121 | func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] 122 | 123 | /** 124 | This method is called by `TableScheme` when a dragged cell hovers over 125 | an index path. 126 | 127 | - parameter tableView: The `UITableView` under the hovering dragged cell 128 | - parameter session: The `UIDropSession` that holds the dragged item 129 | - parameter destinationIndexPath: The index path over which the dragged cell is hovering 130 | - returns: An array of `UIDragItem` which represents the dragged row 131 | */ 132 | @available(iOS 11.0, *) 133 | func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal 134 | 135 | /** 136 | This method is called by `TableScheme` when a dragged cell is dropped. 137 | 138 | - parameter tableView: The `UITableView` under the hovering dragged cell 139 | - parameter coordinator: The coordinator object that handles the drop 140 | */ 141 | @available(iOS 11.0, *) 142 | func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) 143 | } 144 | 145 | extension Scheme { 146 | 147 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, relativeIndex: Int) -> UITableViewCell { 148 | let identifier = reuseIdentifier(forRelativeIndex: relativeIndex) 149 | return tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) 150 | } 151 | 152 | public func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 153 | return .useTable 154 | } 155 | 156 | @available(iOS 11.0, *) 157 | public func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { 158 | return [] 159 | } 160 | 161 | @available(iOS 11.0, *) 162 | public func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { 163 | return UITableViewDropProposal(operation: .cancel) 164 | } 165 | 166 | @available(iOS 11.0, *) 167 | public func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /TableSchemer/SchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/24/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | public protocol SchemeBuilder: AnyObject { 10 | 11 | associatedtype SchemeType: Scheme 12 | 13 | init() 14 | func createScheme() throws -> SchemeType 15 | 16 | } 17 | 18 | public enum SchemeBuilderError: Error { 19 | case missingRequiredAttribute(String) 20 | } 21 | -------------------------------------------------------------------------------- /TableSchemer/SchemeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeCell.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SchemeCell: UITableViewCell { 12 | open var scheme: Scheme? 13 | } 14 | -------------------------------------------------------------------------------- /TableSchemer/SchemeRowAnimators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeRowAnimators.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/18/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | An instance of this class is passed into the closure for explicitly animating rows of a scheme. It records the animation methods 13 | called and then batches them to the passed in UITableView. 14 | */ 15 | public class SchemeRowAnimator { 16 | fileprivate struct AddRemove { 17 | let animation: UITableView.RowAnimation 18 | let index: Int 19 | } 20 | 21 | fileprivate struct Move { 22 | let fromIndex: Int 23 | let toIndex: Int 24 | } 25 | 26 | private final let tableScheme: TableScheme 27 | private final let tableView: UITableView 28 | final let attributedScheme: AttributedScheme 29 | public final var scheme: Scheme { 30 | return attributedScheme.scheme 31 | } 32 | 33 | fileprivate var moves = [Move]() 34 | fileprivate var insertions = [AddRemove]() 35 | fileprivate var deletions = [AddRemove]() 36 | 37 | init(tableScheme: TableScheme, with attributedScheme: AttributedScheme, in tableView: UITableView) { 38 | self.tableScheme = tableScheme 39 | self.attributedScheme = attributedScheme 40 | self.tableView = tableView 41 | } 42 | 43 | /** 44 | Records the row at index to move to toIndex at the end of the batch closure. 45 | 46 | The indexes are relative to the scheme, and cells above or below this scheme 47 | should not be considered when making calls to this method. 48 | 49 | - parameter index: The index to move the row from. 50 | - parameter toIndex: The index to move the row to. 51 | */ 52 | public final func moveObject(at index: Int, to toIndex: Int) { 53 | moves.append(Move(fromIndex: index, toIndex: toIndex)) 54 | } 55 | 56 | /** 57 | Records the row to be removed from index using animation at the end of the batch closure. 58 | 59 | The indexes are relative to the scheme, and cells above or below this scheme 60 | should not be considered when making calls to this method. 61 | 62 | - parameter index: The index to remove. 63 | - parameter rowAnimation: The type of animation to perform. 64 | */ 65 | public final func deleteObject(at index: Int, with rowAnimation: UITableView.RowAnimation = .automatic) { 66 | deletions.append(AddRemove(animation: rowAnimation, index: index)) 67 | } 68 | 69 | /** 70 | Records the row to be inserted to index using animation at the end of the batch closure. 71 | 72 | The indexes are relative to the scheme, and cells above or below this scheme 73 | should not be considered when making calls to this method. 74 | 75 | - parameter index: The index to insert. 76 | - parameter rowAnimation: The type of animation to perform. 77 | */ 78 | public final func insertObject(at index: Int, with rowAnimation: UITableView.RowAnimation = .automatic) { 79 | insertions.append(AddRemove(animation: rowAnimation, index: index)) 80 | } 81 | 82 | /** 83 | Records a range of rows to be removed from indexes using animation at the end of the batch closure. 84 | 85 | The indexes are relative to the scheme, and cells above or below this scheme 86 | should not be considered when making calls to this method. 87 | 88 | - parameter indexes: The indexes to remove. 89 | - parameter rowAnimation: The type of animation to perform. 90 | */ 91 | public final func deleteObjects(at indexes: CountableClosedRange, with rowAnimation: UITableView.RowAnimation = .automatic) { 92 | for i in indexes { 93 | deletions.append(AddRemove(animation: rowAnimation, index: i)) 94 | } 95 | } 96 | 97 | /** 98 | Records a range of rows to be inserted to indexes using animation at the end of the batch closure. 99 | 100 | The indexes are relative to the scheme, and cells above or below this scheme 101 | should not be considered when making calls to this method. 102 | 103 | - parameter indexes: The indexes to insert. 104 | - parameter rowAnimation: The type of animation to perform. 105 | */ 106 | public final func insertObjects(at indexes: CountableClosedRange, with rowAnimation: UITableView.RowAnimation = .automatic) { 107 | for i in indexes { 108 | insertions.append(AddRemove(animation: rowAnimation, index: i)) 109 | } 110 | } 111 | 112 | final func performAnimations() { 113 | let schemeSet = tableScheme.schemeSetWithScheme(scheme) 114 | let section = tableScheme.sectionForSchemeSet(schemeSet) 115 | let rowsBeforeScheme = tableScheme.rowsBeforeScheme(scheme) 116 | 117 | tableView.beginUpdates() 118 | 119 | // Compact our insertions/deletions so we do as few table view animation calls as necessary 120 | let insertRows = insertions.reduce([UITableView.RowAnimation: [IndexPath]]()) { memo, animation in 121 | var memo = memo 122 | if memo[animation.animation] == nil { 123 | memo[animation.animation] = [IndexPath]() 124 | } 125 | 126 | memo[animation.animation]!.append(IndexPath(row: rowsBeforeScheme + animation.index, section: section)) 127 | 128 | return memo 129 | } 130 | 131 | let deleteRows = deletions.reduce([UITableView.RowAnimation: [IndexPath]]()) { memo, animation in 132 | var memo = memo 133 | if memo[animation.animation] == nil { 134 | memo[animation.animation] = [IndexPath]() 135 | } 136 | 137 | memo[animation.animation]!.append(IndexPath(row: rowsBeforeScheme + animation.index, section: section)) 138 | 139 | return memo 140 | } 141 | 142 | // Perform the animations 143 | for move in moves { 144 | tableView.moveRow(at: IndexPath(row: rowsBeforeScheme + move.fromIndex, section: section), to: IndexPath(row: rowsBeforeScheme + move.toIndex, section: section)) 145 | } 146 | 147 | for (animation, insertions) in insertRows { 148 | tableView.insertRows(at: insertions, with: animation) 149 | } 150 | 151 | for (animation, deletions) in deleteRows { 152 | tableView.deleteRows(at: deletions, with: animation) 153 | } 154 | 155 | tableView.endUpdates() 156 | } 157 | } 158 | 159 | final class InferringRowAnimator: SchemeRowAnimator where AnimatableScheme: InferrableRowAnimatableScheme { 160 | private let originalRowIdentifiers: [AnimatableScheme.IdentifierType] 161 | private var animatableScheme: AnimatableScheme { 162 | return scheme as! AnimatableScheme 163 | } 164 | 165 | init(tableScheme: TableScheme, with scheme: AnimatableScheme, ownedBy attributedScheme: AttributedScheme, in tableView: UITableView) { 166 | assert(scheme === attributedScheme.scheme) 167 | originalRowIdentifiers = scheme.rowIdentifiers 168 | super.init(tableScheme: tableScheme, with: attributedScheme, in: tableView) 169 | } 170 | 171 | func guessRowAnimations(with animation: UITableView.RowAnimation) { 172 | let updatedRowIdentifiers = animatableScheme.rowIdentifiers 173 | var addedIdentifiers = updatedRowIdentifiers // Will remove objects when they are found in the original identifiers 174 | var immovableIndexes = Dictionary.Index, Void>() // To help with multiple equal objects 175 | 176 | for (index, identifier) in originalRowIdentifiers.enumerated() { 177 | if let newIndex = findIdentifier(identifier, in: updatedRowIdentifiers, excluding: immovableIndexes) { 178 | // Handle possibility of it moved 179 | if index != newIndex { 180 | // Object was moved 181 | moves.append(Move(fromIndex: index, toIndex: newIndex)) 182 | 183 | } // No animations performed if the index is the same 184 | 185 | // Prevent this index from being considered moved in the future 186 | immovableIndexes[newIndex] = () 187 | 188 | // Object was in both original and updated, so we can remove it from our list of added identifiers 189 | addedIdentifiers.remove(at: addedIdentifiers.firstIndex(of: updatedRowIdentifiers[newIndex])!) 190 | } else { 191 | // Object was deleted, so mark this row deleted 192 | deletions.append(AddRemove(animation: animation, index: index)) 193 | } 194 | } 195 | 196 | for added in addedIdentifiers { 197 | insertions.append(AddRemove(animation: animation, index: updatedRowIdentifiers.firstIndex(of: added)!)) 198 | } 199 | } 200 | 201 | private func findIdentifier(_ identifier: AnimatableScheme.IdentifierType, in identifiers: [AnimatableScheme.IdentifierType], excluding excludedIndexes: Dictionary.Index, Void>) -> Array.Index? { 202 | var foundIndex: Array.Index? 203 | 204 | for (index, ident) in identifiers.enumerated() { 205 | if excludedIndexes[index] == nil && ident == identifier { 206 | foundIndex = index 207 | break 208 | } 209 | } 210 | 211 | return foundIndex 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /TableSchemer/SchemeSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeSet.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | /** 10 | * A SchemeSet is a container for Scheme objects. 11 | * 12 | * This class maps to a table view's section. Its schemes property 13 | * are similar to the rows in that section (though a scheme may 14 | * have more than a single row), and the name property is used for the 15 | * section name. 16 | */ 17 | public class SchemeSet { 18 | 19 | /** This property is the title for the table view section */ 20 | public var headerText: String? 21 | 22 | /** The string returned for tableView:viewForFooterInSection */ 23 | public var footerText: String? 24 | 25 | /** The view to use as the header for the table view section */ 26 | public var headerView: UIView? 27 | 28 | /** The height of the custom header view */ 29 | public var headerViewHeight: RowHeight 30 | 31 | /** The view to use as the footer for the table view section */ 32 | public var footerView: UIView? 33 | 34 | /** The height of the custom footer view */ 35 | public var footerViewHeight: RowHeight 36 | 37 | /** The schemes contained in the SchemeSet */ 38 | var attributedSchemes: [AttributedScheme] 39 | 40 | public var schemes: [Scheme] { 41 | return attributedSchemes.map { $0.scheme } 42 | } 43 | 44 | /** The number of schemes within the SchemeSet */ 45 | public var count: Int { 46 | return schemes.count 47 | } 48 | 49 | /// Schemes that are currently visible 50 | public var visibleSchemes: [Scheme] { 51 | return attributedSchemes.compactMap { $0.hidden ? nil : $0.scheme } 52 | } 53 | 54 | public init(attributedSchemes: [AttributedScheme], 55 | headerText: String? = nil, 56 | footerText: String? = nil, 57 | headerView: UIView? = nil, 58 | headerViewHeight: RowHeight = .useTable, 59 | footerView: UIView? = nil, 60 | footerViewHeight: RowHeight = .useTable) { 61 | self.attributedSchemes = attributedSchemes 62 | self.headerText = headerText 63 | self.footerText = footerText 64 | self.headerView = headerView 65 | self.headerViewHeight = headerViewHeight 66 | self.footerView = footerView 67 | self.footerViewHeight = footerViewHeight 68 | } 69 | 70 | public convenience init(schemes: [Scheme], 71 | headerText: String? = nil, 72 | footerText: String? = nil, 73 | headerView: UIView? = nil, 74 | headerViewHeight: RowHeight = .useTable, 75 | footerView: UIView? = nil, 76 | footerViewHeight: RowHeight = .useTable) { 77 | self.init(attributedSchemes: schemes.map { AttributedScheme(scheme: $0, hidden: false) }, headerText: headerText, footerText: footerText, headerView: headerView, headerViewHeight: headerViewHeight, footerView: footerView, footerViewHeight: footerViewHeight) 78 | } 79 | 80 | public subscript(index: Int) -> Scheme { 81 | return schemes[index] 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /TableSchemer/SchemeSetBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeSetBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** This class facilitates building the contents of a `SchemeSet`. 12 | 13 | An instance of this object is passed into the build handler from `TableSchemeBuilder.buildSchemeSet(handler:)`. 14 | It's used to set a section title and to add schemes to the scheme set. 15 | */ 16 | public final class SchemeSetBuilder { 17 | 18 | /** This will be used as the SchemeSet's header text. If left nil, the SchemeSet will not have a title. */ 19 | public var headerText: String? 20 | 21 | /** This will be used as the SchemeSet's footer text. If left nil, it will not have a footer label */ 22 | public var footerText: String? 23 | 24 | /** This will be used as the SchemeSet's header view. If left nil, it will not have a custom section header view */ 25 | public var headerView: UIView? 26 | 27 | /** This will be used as the SchemeSet's header view height. Default is `.useTable`, which means the height will be calculated automatically based on the content. */ 28 | public var headerViewHeight: RowHeight = .useTable 29 | 30 | /** This will be used as the SchemeSet's footer view. If left nil, it will not have a custom section header view */ 31 | public var footerView: UIView? 32 | 33 | /** This will be used as the SchemeSet's footer view height. Default is `.useTable`, which means the height will be calculated automatically based on the content. */ 34 | public var footerViewHeight: RowHeight = .useTable 35 | 36 | /** These are the Scheme objects that the SchemeSet will be instantiated with. */ 37 | public var schemes: [Scheme] { 38 | return attributedSchemes.map { $0.scheme } 39 | } 40 | 41 | var attributedSchemes = [AttributedScheme]() 42 | 43 | /// This is used to identify if the scheme is initially hidden or not 44 | public var hidden = false 45 | 46 | /** 47 | Build a scheme within the closure. 48 | 49 | This method will instantiate a `Scheme` object, and then pass it into handler. The type of Scheme object that is instantiated will be inferred from the type passed into the handler. 50 | 51 | The created `Scheme` object will be validated before being added to the list of schemes to be created. 52 | 53 | The created `Scheme` object will be returned if you need a reference to it, but it will be added to the `TableScheme` automatically. 54 | 55 | - parameter handler: The closure to configure the scheme. 56 | - returns: The created Scheme instance. 57 | */ 58 | @discardableResult 59 | public func buildScheme(_ handler: (_ builder: BuilderType, _ hidden: inout Bool) -> Void) -> BuilderType.SchemeType { 60 | let builder = BuilderType() 61 | var hidden = false 62 | handler(builder, &hidden) 63 | 64 | let scheme = try! builder.createScheme() 65 | attributedSchemes.append(AttributedScheme(scheme: scheme, hidden: hidden)) 66 | return scheme 67 | } 68 | 69 | @discardableResult 70 | public func buildScheme(_ handler: (_ builder: BuilderType) -> Void) -> BuilderType.SchemeType { 71 | let builder = BuilderType() 72 | handler(builder) 73 | 74 | let scheme = try! builder.createScheme() 75 | attributedSchemes.append(AttributedScheme(scheme: scheme, hidden: false)) 76 | return scheme 77 | } 78 | 79 | /** Create the `SchemeSet` with the currently added `Scheme`s. This method should not be called except from `TableSchemeBuilder` */ 80 | internal func createSchemeSet() -> AttributedSchemeSet { 81 | return AttributedSchemeSet(schemeSet: SchemeSet(attributedSchemes: attributedSchemes, headerText: headerText, footerText: footerText, headerView: headerView, headerViewHeight: headerViewHeight, footerView: footerView, footerViewHeight: footerViewHeight), hidden: hidden) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TableSchemer/StaticScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by Jacob Berkman on 2015-03-20. 6 | // Copyright (c) 2015 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class StaticScheme: Scheme, InferrableReuseIdentifierScheme { 12 | 13 | public typealias SelectionHandler = (_ cell: CellType, _ scheme: StaticScheme) -> Void 14 | 15 | /// The precreated cell to use. 16 | open var cell: CellType 17 | 18 | /// The height the cell should be if asked. 19 | open var height: RowHeight = .useTable 20 | 21 | /** 22 | The closure called when the cell is selected. 23 | 24 | NOTE: This is only called if the TableScheme is asked to handle selection 25 | by the table view delegate. 26 | */ 27 | open var selectionHandler: SelectionHandler? 28 | 29 | /// StaticScheme's always represent a single cell 30 | open var numberOfCells: Int { return 1 } 31 | 32 | public init(cell: CellType) { 33 | self.cell = cell 34 | } 35 | 36 | open func configureCell(_ cell: UITableViewCell, withRelativeIndex relativeIndex: Int) { 37 | // noop, static cells should be configured externally 38 | } 39 | 40 | open func selectCell(_ cell: UITableViewCell, inTableView tableView: UITableView, inSection section: Int, havingRowsBeforeScheme rowsBeforeScheme: Int, withRelativeIndex relativeIndex: Int) { 41 | if let sh = selectionHandler { 42 | sh(cell as! CellType, self) 43 | } 44 | } 45 | 46 | open func reuseIdentifier(forRelativeIndex relativeIndex: Int) -> String { 47 | return String(describing: CellType.self) 48 | } 49 | 50 | open func height(forRelativeIndex relativeIndex: Int) -> RowHeight { 51 | return height 52 | } 53 | 54 | open var reusePairs: [(identifier: String, cellType: UITableViewCell.Type)] { 55 | return [(identifier: String(describing: CellType.self), cellType: CellType.self)] 56 | } 57 | 58 | /// Overriding the default implementation to return our specific cell 59 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, relativeIndex: Int) -> UITableViewCell { 60 | return cell 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /TableSchemer/StaticSchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticSchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/25/15. 6 | // Copyright © 2015 Weebly. All rights reserved. 7 | // 8 | 9 | open class StaticSchemeBuilder: SchemeBuilder { 10 | 11 | public typealias SchemeType = StaticScheme 12 | 13 | public required init() {} 14 | 15 | public func createScheme() throws -> SchemeType { 16 | guard let cell = cell else { 17 | throw SchemeBuilderError.missingRequiredAttribute("cell") 18 | } 19 | 20 | let scheme = SchemeType(cell: cell) 21 | scheme.height = height 22 | scheme.selectionHandler = selectionHandler 23 | return scheme 24 | } 25 | 26 | open var cell: CellType? 27 | open var selectionHandler: SchemeType.SelectionHandler? 28 | open var height: RowHeight = .useTable 29 | 30 | } 31 | -------------------------------------------------------------------------------- /TableSchemer/TableScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableScheme.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | This class can be used as the data source to a UITableView's dataSource. It 13 | is driven by scheme sets, which are mapped directly to a section in the table 14 | view's index paths. Inside each scheme set is an array of schemes. These 15 | schemes provide varying functionlity and allow you to encapsulate all 16 | information about a particular cell, such as a configuration block or row height, in an object. 17 | */ 18 | public class TableScheme: NSObject { 19 | 20 | public typealias BuildHandler = (_ builder: TableSchemeBuilder) -> Void 21 | public internal(set) var attributedSchemeSets: [AttributedSchemeSet] 22 | 23 | /** 24 | A block that gets forwarded from the `scrollViewDidScroll(_:)` delegate method. Make sure to avoid retain cycles by specifing `[weak self]` if necessary. 25 | */ 26 | public var scrollViewDidScrollHandler: ((_ scrollView: UIScrollView) -> Void)? 27 | 28 | /** 29 | A block that gets forwarded from the `scrollViewDidEndDecelerating(_:)` delegate method. Make sure to avoid retain cycles by specifing `[weak self]` if necessary. 30 | */ 31 | public var scrollViewDidEndDeceleratingHandler: ((_ scrollView: UIScrollView) -> Void)? 32 | 33 | #if DEBUG 34 | private var buildingBatchAnimations = false 35 | #endif 36 | 37 | public convenience init(tableView: UITableView, allowReordering: Bool = false, schemeSets: [SchemeSet]) { 38 | self.init(tableView: tableView, allowReordering: allowReordering, attributedSchemeSets: schemeSets.map { AttributedSchemeSet(schemeSet: $0, hidden: false) }) 39 | } 40 | 41 | public init(tableView: UITableView, allowReordering: Bool = false, attributedSchemeSets: [AttributedSchemeSet]) { 42 | self.attributedSchemeSets = attributedSchemeSets 43 | super.init() 44 | 45 | for attributedSchemeSet in attributedSchemeSets { 46 | for attributedScheme in attributedSchemeSet.schemeSet.attributedSchemes { 47 | guard let scheme = attributedScheme.scheme as? InferrableReuseIdentifierScheme else { 48 | continue 49 | } 50 | 51 | for pair in scheme.reusePairs { 52 | tableView.register(pair.cellType, forCellReuseIdentifier: pair.identifier) 53 | } 54 | } 55 | } 56 | 57 | tableView.dataSource = self 58 | tableView.delegate = self 59 | if #available(iOS 11.0, *), allowReordering { 60 | tableView.dragDelegate = self 61 | tableView.dropDelegate = self 62 | tableView.dragInteractionEnabled = true 63 | } 64 | } 65 | 66 | public convenience init(tableView: UITableView, allowReordering: Bool = false, buildHandler: BuildHandler) { 67 | let builder = TableSchemeBuilder() 68 | buildHandler(builder) 69 | self.init(tableView: tableView, allowReordering: allowReordering, attributedSchemeSets: builder.schemeSets) 70 | } 71 | 72 | // MARK: Public Instance Methods 73 | 74 | /** 75 | This method returns the scheme at a given index path. Use this method 76 | in table view delegate methods to find the scheme the cell belongs to. 77 | 78 | - parameter indexPath: The index path that will be used to find the scheme 79 | 80 | - returns: The scheme at the index path. 81 | */ 82 | public func schemeAtIndexPath(_ indexPath: IndexPath) -> Scheme? { 83 | guard let schemeSet = schemeSet(forSection: indexPath.section) else { return nil } 84 | let row = indexPath.row 85 | var offset = 0 86 | var priorHiddenSchemes = 0 87 | 88 | for (idx, attributedScheme) in schemeSet.attributedSchemes.enumerated() { 89 | if attributedScheme.hidden { 90 | priorHiddenSchemes += 1 91 | continue 92 | } 93 | 94 | if row >= (idx + offset - priorHiddenSchemes) && row < (idx + offset + attributedScheme.scheme.numberOfCells - priorHiddenSchemes) { 95 | return attributedScheme.scheme 96 | } else { 97 | offset += attributedScheme.scheme.numberOfCells - 1 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | /** 105 | Returns the `SchemeSet` located at the given index. If one cannot be found, returns nil. 106 | 107 | - parameter section: The section the `SchemeSet` is located at. 108 | - returns: The `SchemeSet` at the given index, or nil if not found. 109 | */ 110 | public func schemeSet(forSection section: Int) -> SchemeSet? { 111 | var schemeSetIndex: Int? 112 | var offset = 0 113 | for (index, schemeSet) in attributedSchemeSets.enumerated() { 114 | // Section indexes do not include our hidden scheme sets, so 115 | // when we pull one from our schemeSets array, which does include 116 | // the hidden scheme sets, we need to offset by our hidden schemes 117 | // before it. 118 | if schemeSet.hidden { 119 | offset += 1 120 | continue 121 | } 122 | 123 | // If our enumerated index minus our prior hidden scheme sets 124 | // equals the section that we're looking for, we found our 125 | // correct scheme set and can end the loop 126 | if index - offset == section { 127 | schemeSetIndex = index 128 | break 129 | } 130 | } 131 | 132 | guard let index = schemeSetIndex else { return nil } 133 | 134 | return attributedSchemeSets[index].schemeSet 135 | } 136 | 137 | /** 138 | This method returns the scheme contained in a particular view. You would typically use 139 | this method when you have a UIControl sending an action for a view and you need to 140 | determine the scheme that contains the control. 141 | 142 | This view must be contained in the view hierarchey for the UITableView that this 143 | TableScheme is backing. 144 | 145 | - parameter view: The view that is contained in the view. 146 | 147 | - returns: The Scheme that contains the view, or nil if the view does not have a scheme. 148 | */ 149 | public func scheme(containing view: UIView) -> Scheme? { 150 | if let cell = view.TSR_containingTableViewCell(), 151 | let tableView = cell.TSR_containingTableView(), 152 | let indexPath = tableView.indexPath(for: cell), 153 | let scheme = schemeAtIndexPath(indexPath) { 154 | return scheme 155 | } 156 | 157 | return nil 158 | } 159 | 160 | /** 161 | This method returns the scheme contained in a particular view along with the offset within 162 | the scheme the chosen cell is at. Its similar to schemeContainingView(view:) -> Scheme?. 163 | 164 | You would typically use this method when you have a UIControl sending an action for a view 165 | that is part of a collection of cells within a scheme. 166 | 167 | This view must be contained in the view hierarchey for the UITableView that this 168 | TableScheme is backing. 169 | 170 | - parameter view: The view that is contained in the view. 171 | 172 | - returns: A tuple with the Scheme and Index of the cell within the scheme, or nil if 173 | the view does not have a scheme. 174 | */ 175 | public func schemeWithIndex(containing view: UIView) -> (scheme: Scheme, index: Int)? { 176 | if let cell = view.TSR_containingTableViewCell(), 177 | let tableView = cell.TSR_containingTableView(), 178 | let indexPath = tableView.indexPath(for: cell), 179 | let scheme = schemeAtIndexPath(indexPath) { 180 | let numberOfRowsBeforeScheme = rowsBeforeScheme(scheme) 181 | let offset = indexPath.row - numberOfRowsBeforeScheme 182 | return (scheme: scheme, index: offset) 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // MARK: Scheme Visibility 189 | 190 | /** 191 | Returns if the given `SchemeSet` is hidden. If the `SchemeSet` does not belong 192 | to the `TableScheme` this will return `nil`. 193 | 194 | - parameter schemeSet: The `SchemeSet` to check visibility for. 195 | - returns: `true` if the `SchemeSet` is hidden, `false` if it is not, and `nil` 196 | if the given `SchemeSet` does not belong to this `TableScheme`. 197 | */ 198 | public func isSchemeSetHidden(_ schemeSet: SchemeSet) -> Bool? { 199 | guard let attributedSchemeSet = attributedSchemeSets.lazy.filter({ $0.schemeSet === schemeSet}).first else { return nil } 200 | return attributedSchemeSet.hidden 201 | } 202 | 203 | /** 204 | Returns if the given `Scheme` is hidden. If the `Scheme` does not belong 205 | to the `TableScheme` this will return `nil`. 206 | 207 | If the `Scheme` is not marked as hidden, but the containing `SchemeSet` is, this 208 | will return `false`. 209 | 210 | - parameter scheme: The `Scheme` to check visibility for. 211 | - returns: `true` if the `Scheme` is hidden, `false` if it is not, and `nil` 212 | if the given `Scheme` does not belong to this `TableScheme`. 213 | */ 214 | public func isSchemeHidden(_ scheme: Scheme) -> Bool? { 215 | for attributedSchemeSet in attributedSchemeSets { 216 | for attributedScheme in attributedSchemeSet.schemeSet.attributedSchemes { 217 | if attributedScheme.scheme === scheme { 218 | return attributedScheme.hidden 219 | } 220 | } 221 | } 222 | 223 | return nil 224 | } 225 | 226 | /** 227 | Hides a Scheme in the provided table view using the given animation. 228 | 229 | The passed in Scheme must belong to the TableScheme. 230 | 231 | - parameter scheme: The scheme to hide. 232 | - parameter tableView: The UITableView to perform the animations on. 233 | - parameter rowAnimation: The type of animation that should be performed. 234 | */ 235 | public func hideScheme(_ scheme: Scheme, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 236 | #if DEBUG 237 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 238 | #endif 239 | 240 | guard let indexes = attributedSchemeIndexesWithScheme(scheme) else { 241 | NSLog("ERROR: Could not locate \(scheme) within \(self)") 242 | return 243 | } 244 | 245 | attributedSchemeSets[indexes.schemeSetIndex].schemeSet.attributedSchemes[indexes.schemeIndex].hidden = true 246 | tableView.deleteRows(at: indexPathsForScheme(scheme), with: rowAnimation) 247 | } 248 | 249 | /** 250 | Shows a Scheme in the provided table view using the given animation. 251 | 252 | The passed in Scheme must belong to the TableScheme. 253 | 254 | - parameter scheme: The scheme to show. 255 | - parameter tableView: The UITableView to perform the animations on. 256 | - parameter rowAnimation: The type of animation that should be performed. 257 | */ 258 | public func showScheme(_ scheme: Scheme, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 259 | #if DEBUG 260 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 261 | #endif 262 | 263 | guard let indexes = attributedSchemeIndexesWithScheme(scheme) else { 264 | NSLog("ERROR: Could not locate \(scheme) within \(self)") 265 | return 266 | } 267 | 268 | attributedSchemeSets[indexes.schemeSetIndex].schemeSet.attributedSchemes[indexes.schemeIndex].hidden = false 269 | tableView.insertRows(at: indexPathsForScheme(scheme), with: rowAnimation) 270 | } 271 | 272 | /** 273 | Reloads a Scheme in the provided table view using the given animation. 274 | 275 | The passed in Scheme must belong to the TableScheme. 276 | 277 | - parameter scheme: The scheme to reload the rows for. 278 | - parameter tableView: The UITableView to perform the animations on. 279 | - parameter rowAnimation: The type of animation that should be performed. 280 | */ 281 | public func reloadScheme(_ scheme: Scheme, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 282 | #if DEBUG 283 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 284 | #endif 285 | 286 | guard let indexes = attributedSchemeIndexesWithScheme(scheme) else { 287 | NSLog("ERROR: Could not locate \(scheme) within \(self)") 288 | return 289 | } 290 | 291 | if !attributedSchemeSets[indexes.schemeSetIndex].schemeSet.attributedSchemes[indexes.schemeIndex].hidden { 292 | tableView.reloadRows(at: indexPathsForScheme(scheme), with: rowAnimation) 293 | } 294 | } 295 | 296 | /** 297 | Hides a SchemeSet in the provided table view using the given animation. 298 | 299 | The passed in SchemeSet must belong to the TableScheme. This method should not be used with batch updates. Instead, use 300 | `hideSchemeSet(_:, with:)`. 301 | 302 | - parameter schemeSet: The schemeSet to hide. 303 | - parameter tableView: The UITableView to perform the animations on. 304 | - parameter rowAnimation: The type of animation that should be performed. 305 | */ 306 | public func hideSchemeSet(_ schemeSet: SchemeSet, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 307 | #if DEBUG 308 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 309 | #endif 310 | 311 | guard let index = attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 312 | NSLog("ERROR: Could not locate \(schemeSet) within \(self)") 313 | return 314 | } 315 | 316 | let section = sectionForSchemeSet(schemeSet) 317 | attributedSchemeSets[index].hidden = true 318 | tableView.deleteSections(IndexSet(integer: section), with: rowAnimation) 319 | } 320 | 321 | /** 322 | Shows a SchemeSet in the provided table view using the given animation. 323 | 324 | The passed in SchemeSet must belong to the TableScheme. 325 | 326 | - parameter schemeSet: The schemeSet to show. 327 | - parameter tableView: The UITableView to perform the animations on. 328 | - parameter rowAnimation: The type of animation that should be performed. 329 | */ 330 | public func showSchemeSet(_ schemeSet: SchemeSet, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 331 | #if DEBUG 332 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 333 | #endif 334 | 335 | guard let index = attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 336 | NSLog("ERROR: Could not locate \(schemeSet) within \(self)") 337 | return 338 | } 339 | 340 | let section = sectionForSchemeSet(schemeSet) 341 | attributedSchemeSets[index].hidden = false 342 | tableView.insertSections(IndexSet(integer: section), with: rowAnimation) 343 | } 344 | 345 | /** 346 | Reloads a SchemeSet in the provided table view using the given animation. 347 | 348 | The passed in SchemeSet must belong to the TableScheme. 349 | 350 | - parameter schemeSet: The schemeSet to reload the rows for. 351 | - parameter tableView: The UITableView to perform the animations on. 352 | - parameter rowAnimation: The type of animation that should be performed. 353 | */ 354 | public func reloadSchemeSet(_ schemeSet: SchemeSet, in tableView: UITableView, with rowAnimation: UITableView.RowAnimation = .automatic) { 355 | #if DEBUG 356 | assert(!buildingBatchAnimations, "You should not use this method within a batch update block") 357 | #endif 358 | 359 | guard let index = attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 360 | NSLog("ERROR: Could not locate \(schemeSet) within \(self)") 361 | return 362 | } 363 | 364 | if !attributedSchemeSets[index].hidden { 365 | tableView.reloadSections(IndexSet(integer: sectionForSchemeSet(schemeSet)), with: rowAnimation) 366 | } 367 | } 368 | 369 | 370 | /** 371 | Perform batch changes to the given table view using the operations performed on the animator passed in the 372 | visibilityOperations closure. 373 | 374 | It's important that this method be used over explicitly calling beginUpdates()/endUpdates() and using the normal 375 | visibility operations. The normal visibility operations will update the data set immediately, while the batch-specific 376 | visibility operations (which are part of the passed in BatchAnimator class) will defer 377 | determining all the indexPaths until it is time to update the table view. 378 | 379 | - parameter tableView: The UITableView to perform the animations on. 380 | - parameter visibilityOperations: A closure containing the animation operations to be performed on the UITableView. A BatchAnimator 381 | will be passed into the closure, which is where your batch operations should occur. 382 | */ 383 | public func batchSchemeVisibilityChanges(in tableView: UITableView, visibilityOperations: (_ animator: TableSchemeBatchAnimator) -> Void) { 384 | let batchAnimator = TableSchemeBatchAnimator(tableScheme: self, withTableView: tableView) 385 | tableView.beginUpdates() 386 | 387 | #if DEBUG 388 | buildingBatchAnimations = true 389 | #endif 390 | 391 | visibilityOperations(batchAnimator) 392 | 393 | #if DEBUG 394 | buildingBatchAnimations = false 395 | #endif 396 | 397 | batchAnimator.performVisibilityChanges() 398 | tableView.endUpdates() 399 | } 400 | 401 | /** 402 | This method will perform animations instructed in the changeHandler on the passed in SchemeRowAnimator. It allows you to have complete 403 | control over how changes in a scheme are animated. 404 | 405 | Note this method does not make any changes to your scheme, and only provides you a way to make the changes to the table view based 406 | on the schemes relative index paths. For example, if you insert a row on the SchemeRowAnimator at index 0, and the scheme starts at 407 | row 2 in section 3, it will perform an insertion at row 2 in section 3. This allows you to think about how your scheme animates 408 | internally, and ignore how schemes around it are laid out. It's recommended that you make the changes to your scheme within the 409 | changeHandler block as well to keep that code grouped together. 410 | 411 | - parameter scheme: The scheme that the changes are being applied to. 412 | - parameter tableView: The UITableView that the animations should be performed on. 413 | - parameter changeHandler: A closure with a SchemeRowAnimator that you give your animation instructions to. 414 | */ 415 | public func animateChangesToScheme(_ scheme: Scheme, inTableView tableView: UITableView, withChangeHandler changeHandler: (_ animator: SchemeRowAnimator) -> Void) { 416 | guard let indexes = attributedSchemeIndexesWithScheme(scheme) else { 417 | NSLog("ERROR: Could not locate \(scheme) within \(self)") 418 | return 419 | } 420 | 421 | let animator = SchemeRowAnimator(tableScheme: self, with: attributedSchemeSets[indexes.schemeSetIndex].schemeSet.attributedSchemes[indexes.schemeIndex], in: tableView) 422 | changeHandler(animator) 423 | animator.performAnimations() 424 | } 425 | 426 | /** 427 | This method will infer changes done to a scheme that conforms to InferrableRowAnimatableScheme within the changeHandler, and 428 | perform appropriate animations to the passed in tableView. 429 | 430 | You must make your changes to the scheme within the changeHandler, or the animation object will not be able to identify the difference 431 | between the object before and after the closure. 432 | 433 | The changes to your object must affect the rowIdentifiers property of the InferrableRowAnimatableScheme protocol. You 434 | must have one rowIdentifier for each row. 435 | 436 | - parameter scheme: The scheme that the changes are being applied to. 437 | - parameter tableView: The UITableView that the animations should be performed on. 438 | - parameter changeHandler: A closure that you perform the changes to your scheme in. 439 | */ 440 | public func animateChangesToScheme(_ scheme: T, inTableView tableView: UITableView, withAnimation animation: UITableView.RowAnimation = .automatic, withChangeHandler changeHandler: () -> Void) where T: InferrableRowAnimatableScheme { 441 | guard let indexes = attributedSchemeIndexesWithScheme(scheme) else { 442 | NSLog("ERROR: Could not locate \(scheme) within \(self)") 443 | return 444 | } 445 | 446 | let animator = InferringRowAnimator(tableScheme: self, with: scheme, ownedBy: attributedSchemeSets[indexes.schemeSetIndex].schemeSet.attributedSchemes[indexes.schemeIndex], in: tableView) 447 | assert(scheme.rowIdentifiers.count == scheme.numberOfCells, "The schemes number of row identifiers must equal its number of cells before the changes") 448 | changeHandler() 449 | assert(scheme.rowIdentifiers.count == scheme.numberOfCells, "The schemes number of row identifiers must equal its number of cells after the changes") 450 | animator.guessRowAnimations(with: animation) 451 | animator.performAnimations() 452 | } 453 | 454 | /** 455 | Locates the index for a given `SchemeSet`. 456 | 457 | - parameter schemeSet: The `SchemeSet` to locate the index for 458 | - returns: The index of the scheme set, or nil if it doesn't exist 459 | */ 460 | public func attributedSchemeSetIndexForSchemeSet(_ schemeSet: SchemeSet) -> Array.Index? { 461 | return attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) 462 | } 463 | 464 | /** 465 | Locates the indexes for a given `Scheme`. 466 | 467 | - parameter scheme: The `Scheme` to locate the indexes for 468 | - returns: The index of the scheme and scheme set, or nil if it doesn't exist 469 | */ 470 | public func attributedSchemeIndexesWithScheme(_ scheme: Scheme) -> (schemeSetIndex: Array.Index, schemeIndex: Array.Index)? { 471 | for (attributedSchemeSetIndex, attributedSchemeSet) in attributedSchemeSets.enumerated() { 472 | for (attributedSchemeIndex, attributedScheme) in attributedSchemeSet.schemeSet.attributedSchemes.enumerated() { 473 | if attributedScheme.scheme === scheme { 474 | return (schemeSetIndex: attributedSchemeSetIndex, schemeIndex: attributedSchemeIndex) 475 | } 476 | } 477 | } 478 | 479 | return nil 480 | } 481 | 482 | 483 | // MARK: - Internal methods 484 | 485 | func indexPathsForScheme(_ scheme: Scheme) -> [IndexPath] { 486 | let rbs = rowsBeforeScheme(scheme) 487 | let schemeSet = schemeSetWithScheme(scheme) 488 | let section = sectionForSchemeSet(schemeSet) 489 | return (rbs..<(rbs + scheme.numberOfCells)).map { IndexPath(row: $0, section: section) } 490 | } 491 | 492 | func sectionForSchemeSet(_ schemeSet: SchemeSet) -> Int { 493 | var i = 0 494 | 495 | for scanSchemeSet in attributedSchemeSets { 496 | if scanSchemeSet.schemeSet === schemeSet { 497 | return i 498 | } else { 499 | if !scanSchemeSet.hidden { 500 | i += 1 501 | } 502 | } 503 | } 504 | 505 | return i 506 | } 507 | 508 | 509 | func rowsBeforeScheme(_ scheme: Scheme) -> Int { 510 | let schemeSet = schemeSetWithScheme(scheme) 511 | 512 | var count = 0 513 | for scanAttributedSchemeObject in schemeSet.attributedSchemes { 514 | if scanAttributedSchemeObject.scheme === scheme { 515 | break 516 | } 517 | 518 | if scanAttributedSchemeObject.hidden { 519 | continue 520 | } 521 | 522 | count += scanAttributedSchemeObject.scheme.numberOfCells 523 | } 524 | 525 | return count 526 | } 527 | 528 | func schemeSetWithScheme(_ scheme: Scheme) -> SchemeSet { 529 | var foundSet: AttributedSchemeSet? 530 | 531 | for schemeSet in attributedSchemeSets { 532 | for scanScheme in schemeSet.schemeSet.schemes { 533 | if scanScheme === scheme { 534 | foundSet = schemeSet 535 | break 536 | } 537 | } 538 | } 539 | 540 | assert(foundSet != nil) 541 | 542 | return foundSet!.schemeSet 543 | } 544 | 545 | } 546 | 547 | // MARK: UITableViewDataSource methods 548 | extension TableScheme: UITableViewDataSource { 549 | 550 | public func numberOfSections(in tableView: UITableView) -> Int { 551 | return attributedSchemeSets.reduce(0) { $1.hidden ? $0 : $0 + 1 } 552 | } 553 | 554 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 555 | let set = schemeSet(forSection: section)! 556 | 557 | return set.visibleSchemes.reduce(0) { (memo: Int, scheme: Scheme) in 558 | memo + scheme.numberOfCells 559 | } 560 | } 561 | 562 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 563 | let scheme = schemeAtIndexPath(indexPath)! 564 | let configurationIndex = indexPath.row - rowsBeforeScheme(scheme) 565 | let cell = scheme.tableView(tableView, cellForRowAt: indexPath, relativeIndex: configurationIndex) 566 | 567 | (cell as? SchemeCell)?.scheme = scheme 568 | 569 | scheme.configureCell(cell, withRelativeIndex: configurationIndex) 570 | 571 | return cell 572 | } 573 | 574 | public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 575 | return schemeSet(forSection: section)?.headerText 576 | } 577 | 578 | public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 579 | return schemeSet(forSection: section)?.footerText 580 | } 581 | 582 | } 583 | 584 | extension TableScheme: UITableViewDelegate { 585 | 586 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 587 | guard let scheme = schemeAtIndexPath(indexPath), let cell = tableView.cellForRow(at: indexPath) else { return } 588 | 589 | let numberOfRowsBeforeScheme = rowsBeforeScheme(scheme) 590 | let newSelectedIndex = indexPath.row - numberOfRowsBeforeScheme 591 | scheme.selectCell(cell, inTableView: tableView, inSection: indexPath.section, havingRowsBeforeScheme: numberOfRowsBeforeScheme, withRelativeIndex: newSelectedIndex) 592 | tableView.deselectRow(at: indexPath, animated: true) 593 | } 594 | 595 | public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 596 | let scheme = schemeAtIndexPath(indexPath)! 597 | let relativeIndex = indexPath.row - rowsBeforeScheme(scheme) 598 | let rowHeight = scheme.height(forRelativeIndex: relativeIndex) 599 | 600 | switch rowHeight { 601 | case .useTable: 602 | return tableView.rowHeight 603 | case .custom(let height): 604 | return height 605 | } 606 | } 607 | 608 | public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 609 | return schemeSet(forSection: section)?.headerView 610 | } 611 | 612 | public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 613 | guard let schemeSet = schemeSet(forSection: section) else { 614 | return UITableView.automaticDimension 615 | } 616 | 617 | switch schemeSet.headerViewHeight { 618 | case .useTable: 619 | return tableView.sectionHeaderHeight 620 | case .custom(let height): 621 | return height 622 | } 623 | } 624 | 625 | public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 626 | return schemeSet(forSection: section)?.footerView 627 | } 628 | 629 | public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 630 | guard let schemeSet = schemeSet(forSection: section) else { 631 | return UITableView.automaticDimension 632 | } 633 | 634 | switch schemeSet.footerViewHeight { 635 | case .useTable: 636 | return tableView.sectionFooterHeight 637 | case .custom(let height): 638 | return height 639 | } 640 | } 641 | 642 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 643 | scrollViewDidScrollHandler?(scrollView) 644 | } 645 | 646 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 647 | scrollViewDidEndDeceleratingHandler?(scrollView) 648 | } 649 | 650 | } 651 | 652 | @available(iOS 11.0, *) 653 | extension TableScheme: UITableViewDragDelegate { 654 | 655 | public func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { 656 | return schemeAtIndexPath(indexPath)!.tableView(tableView, itemsForBeginning: session, at: indexPath) 657 | } 658 | 659 | } 660 | 661 | @available(iOS 11.0, *) 662 | extension TableScheme: UITableViewDropDelegate { 663 | 664 | public func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { 665 | guard let destinationIndexPath = destinationIndexPath, let scheme = schemeAtIndexPath(destinationIndexPath) else { return UITableViewDropProposal(operation: .cancel) } 666 | return scheme.tableView(tableView, dropSessionDidUpdate: session, withDestinationIndexPath: destinationIndexPath) 667 | } 668 | 669 | public func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { 670 | 671 | let destinationIndexPath: IndexPath 672 | 673 | if let indexPath = coordinator.destinationIndexPath { 674 | destinationIndexPath = indexPath 675 | } else { 676 | // Get last index path of table view. 677 | let section = tableView.numberOfSections - 1 678 | let row = tableView.numberOfRows(inSection: section) 679 | destinationIndexPath = IndexPath(row: row, section: section) 680 | } 681 | 682 | schemeAtIndexPath(destinationIndexPath)!.tableView(tableView, performDropWith: coordinator) 683 | } 684 | 685 | } 686 | -------------------------------------------------------------------------------- /TableSchemer/TableSchemeBatchAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSchemeBatchAnimator.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/18/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | This class is passed into the closure for performing batch animations to a TableScheme. It will record 13 | your changes to the TableScheme's SchemeSet and Scheme visibility, and then perform them all in one batch 14 | at the end of the TableScheme's batch operation method. 15 | */ 16 | public final class TableSchemeBatchAnimator { 17 | private struct Row { 18 | let animation: UITableView.RowAnimation 19 | let attributedSchemeSetIndex: Array.Index 20 | let attributedSchemeIndex: Array.Index 21 | } 22 | 23 | private struct Section { 24 | let animation: UITableView.RowAnimation 25 | let attributedSchemeSetIndex: Array.Index 26 | } 27 | 28 | private var rowInsertions = [Row]() 29 | private var rowDeletions = [Row]() 30 | private var rowReloads = [Row]() 31 | private var sectionInsertions = [Section]() 32 | private var sectionDeletions = [Section]() 33 | private var sectionReloads = [Section]() 34 | 35 | private let tableScheme: TableScheme 36 | private let tableView: UITableView 37 | 38 | init(tableScheme: TableScheme, withTableView tableView: UITableView) { 39 | self.tableScheme = tableScheme 40 | self.tableView = tableView 41 | } 42 | 43 | /** 44 | Shows a Scheme within a batch update using the given animation. 45 | 46 | The passed in Scheme must belong to the TableScheme. 47 | 48 | - parameter scheme: The scheme to show. 49 | - parameter rowAnimation: The type of animation that should be performed. 50 | */ 51 | public func showScheme(_ scheme: Scheme, with rowAnimation: UITableView.RowAnimation = .automatic) { 52 | let indexes = tableScheme.attributedSchemeIndexesWithScheme(scheme)! 53 | rowInsertions.append(Row(animation: rowAnimation, attributedSchemeSetIndex: indexes.schemeSetIndex, attributedSchemeIndex: indexes.schemeIndex)) 54 | } 55 | 56 | /** 57 | Hides a Scheme within a batch update. 58 | 59 | The passed in Scheme must belong to the TableScheme. 60 | 61 | - parameter scheme: The scheme to hide. 62 | - parameter rowAnimation: The type of animation that should be performed. 63 | */ 64 | public func hideScheme(_ scheme: Scheme, with rowAnimation: UITableView.RowAnimation = .automatic) { 65 | let indexes = tableScheme.attributedSchemeIndexesWithScheme(scheme)! 66 | rowDeletions.append(Row(animation: rowAnimation, attributedSchemeSetIndex: indexes.schemeSetIndex, attributedSchemeIndex: indexes.schemeIndex)) 67 | } 68 | 69 | /** 70 | Reloads a Scheme within a batch update. 71 | 72 | The passed in Scheme must belong to the TableScheme. 73 | 74 | - parameter scheme: The scheme to reload. 75 | - parameter rowAnimation: The type of animation that should be performed. 76 | */ 77 | public func reloadScheme(_ scheme: Scheme, with rowAnimation: UITableView.RowAnimation = .automatic) { 78 | let indexes = tableScheme.attributedSchemeIndexesWithScheme(scheme)! 79 | rowReloads.append(Row(animation: rowAnimation, attributedSchemeSetIndex: indexes.schemeSetIndex, attributedSchemeIndex: indexes.schemeIndex)) 80 | } 81 | 82 | /** 83 | Shows a SchemeSet within a batch update using the given animation. 84 | 85 | The passed in SchemeSet must belong to the TableScheme. 86 | 87 | - parameter schemeSet: The schemeSet to hide. 88 | - parameter rowAnimation: The type of animation that should be performed. 89 | */ 90 | public func showSchemeSet(_ schemeSet: SchemeSet, with rowAnimation: UITableView.RowAnimation = .automatic) { 91 | guard let index = tableScheme.attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 92 | NSLog("ERROR: Could not locate \(schemeSet) within \(tableScheme)") 93 | return 94 | } 95 | 96 | sectionInsertions.append(Section(animation: rowAnimation, attributedSchemeSetIndex: index)) 97 | } 98 | 99 | /** 100 | Hides a SchemeSet within a batch update using the given animation. 101 | 102 | The passed in SchemeSet must belong to the TableScheme. 103 | 104 | - parameter schemeSet: The schemeSet to hide. 105 | - parameter rowAnimation: The type of animation that should be performed. 106 | */ 107 | public func hideSchemeSet(_ schemeSet: SchemeSet, with rowAnimation: UITableView.RowAnimation = .automatic) { 108 | guard let index = tableScheme.attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 109 | NSLog("ERROR: Could not locate \(schemeSet) within \(tableScheme)") 110 | return 111 | } 112 | 113 | sectionDeletions.append(Section(animation: rowAnimation, attributedSchemeSetIndex: index)) 114 | } 115 | 116 | /** 117 | Reloads a SchemeSet within a batch update. 118 | 119 | The passed in SchemeSet must belong to the TableScheme. 120 | 121 | - parameter schemeSet: The schemeSet to reload. 122 | - parameter rowAnimation: The type of animation that should be performed. 123 | */ 124 | public func reloadSchemeSet(_ schemeSet: SchemeSet, with rowAnimation: UITableView.RowAnimation = .automatic) { 125 | guard let index = tableScheme.attributedSchemeSets.firstIndex(where: { $0.schemeSet === schemeSet }) else { 126 | NSLog("ERROR: Could not locate \(schemeSet) within \(tableScheme)") 127 | return 128 | } 129 | 130 | sectionReloads.append(Section(animation: rowAnimation, attributedSchemeSetIndex: index)) 131 | } 132 | 133 | // MARK: - Internal methods 134 | func performVisibilityChanges() { 135 | // Don't notify table view of changes in hidden scheme sets, or scheme sets we're already notifying about. 136 | let hiddenSchemeSets = tableScheme.attributedSchemeSets.filter({ $0.hidden }).map({ $0.schemeSet }) 137 | let mutatedSchemeSets = (sectionDeletions + sectionInsertions + sectionReloads).map { self.tableScheme.attributedSchemeSets[$0.attributedSchemeSetIndex].schemeSet } 138 | let ignoredSchemeSets: [SchemeSet] = hiddenSchemeSets + mutatedSchemeSets 139 | 140 | // Get the index paths of the schemes we are deleting. This will give us the deletion index paths. We need to do 141 | // this before marking them as hidden so indexPathForScheme doesn't skip it 142 | 143 | let deleteRows = rowDeletions.filter { row in 144 | ignoredSchemeSets.firstIndex { self.tableScheme.attributedSchemeSets[row.attributedSchemeSetIndex].schemeSet === $0 } == nil 145 | }.reduce([UITableView.RowAnimation: [IndexPath]]()) { memo, change in 146 | var memo = memo 147 | if memo[change.animation] == nil { 148 | memo[change.animation] = [IndexPath]() 149 | } 150 | 151 | let scheme = self.tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet.attributedSchemes[change.attributedSchemeIndex].scheme 152 | memo[change.animation]! += self.tableScheme.indexPathsForScheme(scheme) 153 | 154 | return memo 155 | } 156 | 157 | let deleteSections = sectionDeletions.reduce([UITableView.RowAnimation: NSMutableIndexSet]()) { memo, change in 158 | var memo = memo 159 | if memo[change.animation] == nil { 160 | memo[change.animation] = NSMutableIndexSet() as NSMutableIndexSet 161 | } 162 | 163 | let schemeSet = tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet 164 | 165 | memo[change.animation]!.add(self.tableScheme.sectionForSchemeSet(schemeSet)) 166 | 167 | return memo 168 | } 169 | 170 | // We also need the index paths of the reloaded schemes and sections before making changes to the table. 171 | 172 | let reloadRows = rowReloads.filter { row in 173 | ignoredSchemeSets.firstIndex { self.tableScheme.attributedSchemeSets[row.attributedSchemeSetIndex].schemeSet === $0 } == nil 174 | }.reduce([UITableView.RowAnimation: [IndexPath]]()) { memo, change in 175 | var memo = memo 176 | if memo[change.animation] == nil { 177 | memo[change.animation] = [IndexPath]() 178 | } 179 | 180 | let scheme = self.tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet.attributedSchemes[change.attributedSchemeIndex].scheme 181 | memo[change.animation]! += self.tableScheme.indexPathsForScheme(scheme) 182 | 183 | return memo 184 | } 185 | 186 | let reloadSections = sectionReloads.reduce([UITableView.RowAnimation: NSMutableIndexSet]()) { memo, change in 187 | var memo = memo 188 | if memo[change.animation] == nil { 189 | memo[change.animation] = NSMutableIndexSet() as NSMutableIndexSet 190 | } 191 | 192 | let schemeSet = tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet 193 | 194 | memo[change.animation]!.add(self.tableScheme.sectionForSchemeSet(schemeSet)) 195 | 196 | return memo 197 | } 198 | 199 | // Now update the visibility of all our batches 200 | 201 | for change in rowInsertions { 202 | tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet.attributedSchemes[change.attributedSchemeIndex].hidden = false 203 | } 204 | 205 | for change in rowDeletions { 206 | tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet.attributedSchemes[change.attributedSchemeIndex].hidden = true 207 | } 208 | 209 | for change in sectionDeletions { 210 | tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].hidden = true 211 | } 212 | 213 | for change in sectionInsertions { 214 | tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].hidden = false 215 | } 216 | 217 | // Now obtain the index paths for the inserted schemes. These will have their inserted index paths, skipping ones removed, 218 | // and correctly finding the ones that are visible 219 | 220 | let insertRows = rowInsertions.filter { row in 221 | ignoredSchemeSets.firstIndex { self.tableScheme.attributedSchemeSets[row.attributedSchemeSetIndex].schemeSet === $0 } == nil 222 | }.reduce([UITableView.RowAnimation: [IndexPath]]()) { memo, change in 223 | var memo = memo 224 | if memo[change.animation] == nil { 225 | memo[change.animation] = [IndexPath]() 226 | } 227 | 228 | let scheme = self.tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet.attributedSchemes[change.attributedSchemeIndex].scheme 229 | memo[change.animation]! += self.tableScheme.indexPathsForScheme(scheme) 230 | 231 | return memo 232 | } 233 | 234 | let insertSections = sectionInsertions.reduce([UITableView.RowAnimation: NSMutableIndexSet]()) { memo, change in 235 | var memo = memo 236 | if memo[change.animation] == nil { 237 | memo[change.animation] = NSMutableIndexSet() as NSMutableIndexSet 238 | } 239 | 240 | let schemeSet = tableScheme.attributedSchemeSets[change.attributedSchemeSetIndex].schemeSet 241 | 242 | memo[change.animation]!.add(self.tableScheme.sectionForSchemeSet(schemeSet)) 243 | 244 | return memo 245 | } 246 | 247 | // Now we have all the data we need to execute our animations. Perform them! 248 | 249 | for (animation, changes) in insertRows { 250 | tableView.insertRows(at: changes, with: animation) 251 | } 252 | 253 | for (animation, changes) in deleteRows { 254 | tableView.deleteRows(at: changes, with: animation) 255 | } 256 | 257 | for (animation, changes) in insertSections { 258 | tableView.insertSections(changes as IndexSet, with: animation) 259 | } 260 | 261 | for (animation, changes) in deleteSections { 262 | tableView.deleteSections(changes as IndexSet, with: animation) 263 | } 264 | 265 | for (animation, changes) in reloadRows { 266 | tableView.reloadRows(at: changes, with: animation) 267 | } 268 | 269 | for (animation, changes) in reloadSections { 270 | tableView.reloadSections(changes as IndexSet, with: animation) 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /TableSchemer/TableSchemeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSchemeBuilder.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | This class facilitates building the contents of a TableScheme. 13 | 14 | An instance of this object is passed into the build handler from TableScheme(buildHandler:). 15 | It's used to create SchemeSet objects, which correspond directly to table view sections. 16 | */ 17 | public final class TableSchemeBuilder { 18 | 19 | /** The scheme sets that have been added to the builder. */ 20 | public var schemeSets = [AttributedSchemeSet]() 21 | 22 | /** 23 | Builds a SchemeSet object with the configured builder passed into the handler. 24 | 25 | This method will instantiate a SchemeSetBuilder object and then pass it into handler. 26 | The method will take the builder object you configure and create a SchemeSet object, which 27 | will be added to the data sources array of scheme sets. 28 | 29 | The created SchemeSet object will be returned if you need a reference to it, but it will 30 | be added to the data source automatically. 31 | 32 | - parameter handler: The block to configure the builder. 33 | - returns: The created SchemeSet object. 34 | */ 35 | @discardableResult public func buildSchemeSet(_ handler: (_ builder: SchemeSetBuilder) -> Void) -> SchemeSet { 36 | let builder = SchemeSetBuilder() 37 | handler(builder) 38 | let schemeSet = builder.createSchemeSet() 39 | schemeSets.append(schemeSet) 40 | return schemeSet.schemeSet 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /TableSchemer/TableSchemer.h: -------------------------------------------------------------------------------- 1 | // 2 | // TableSchemer.h 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 7/20/15. 6 | // Copyright (c) 2015 Weebly. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TableSchemer. 12 | FOUNDATION_EXPORT double TableSchemerVersionNumber; 13 | 14 | //! Project version string for TableSchemer. 15 | FOUNDATION_EXPORT const unsigned char TableSchemerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /TableSchemer/ViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExtensions.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 7/30/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | public func TSR_containingTableViewCell() -> UITableViewCell? { 13 | var view: UIView? = self 14 | while let v = view { 15 | if v is UITableViewCell { 16 | break 17 | } 18 | 19 | view = v.superview 20 | } 21 | 22 | return view as? UITableViewCell 23 | } 24 | } 25 | 26 | extension UITableViewCell { 27 | public func TSR_containingTableView() -> UITableView? { 28 | var view: UIView? = self 29 | while let v = view { 30 | if v is UITableView { 31 | break 32 | } 33 | 34 | view = v.superview 35 | } 36 | 37 | return view as? UITableView 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TableSchemerExamples/AdvancedTableSchemeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedTableSchemeViewController.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 7/2/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import TableSchemer 10 | import UIKit 11 | 12 | class AdvancedTableSchemeViewController: UITableViewController { 13 | let switchReuseIdentifier = "SwitchCell" 14 | let inputReuseIdentifier = "InputCell" 15 | let basicReuseIdentifier = "BasicCell" 16 | var tableScheme: TableScheme! 17 | var firstSwitchScheme: Scheme! 18 | var secondSwitchScheme: Scheme! 19 | var firstFieldScheme: Scheme! 20 | var secondFieldScheme: Scheme! 21 | var buttonsScheme: ArrayScheme! 22 | 23 | var wifiEnabled = false 24 | var bluetoothEnabled = false 25 | 26 | var firstFieldValue = "" 27 | var secondFieldValue = "" 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | title = "Advanced" 32 | tableView.rowHeight = 44.0 33 | 34 | buildAndSetTableScheme() 35 | } 36 | 37 | func buildAndSetTableScheme() { 38 | tableScheme = TableScheme(tableView: tableView) { builder in 39 | builder.buildSchemeSet { builder in 40 | builder.headerText = "Switches" 41 | builder.footerText = "Section footer text" 42 | 43 | firstSwitchScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 44 | scheme.configurationHandler = { [unowned self] cell in 45 | cell.textLabel?.text = "First Switch" 46 | cell.selectionStyle = .none 47 | let switchView = UISwitch() 48 | switchView.isOn = self.wifiEnabled 49 | switchView.addTarget(self, action: #selector(AdvancedTableSchemeViewController.switcherUpdated(_:)), for: .valueChanged) // Don't worry about this being reapplied on reuse; it has checks =) 50 | cell.accessoryView = switchView 51 | } 52 | } 53 | 54 | secondSwitchScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 55 | scheme.configurationHandler = { [unowned self] cell in 56 | cell.textLabel?.text = "Second Switch" 57 | cell.selectionStyle = .none 58 | let switchView = UISwitch() 59 | switchView.isOn = self.bluetoothEnabled 60 | switchView.addTarget(self, action: #selector(AdvancedTableSchemeViewController.switcherUpdated(_:)), for: .valueChanged) 61 | cell.accessoryView = switchView 62 | } 63 | } 64 | 65 | } 66 | 67 | builder.buildSchemeSet { builder in 68 | 69 | let headerView = HeaderFooterView() 70 | headerView.label.text = "Text Input; Actually this is also a test for custom section header views" 71 | builder.headerView = headerView 72 | 73 | let footerView = HeaderFooterView() 74 | footerView.label.text = "Let's also test out the footer view to make sure it works with dynamic sizing" 75 | builder.footerView = footerView 76 | 77 | firstFieldScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 78 | scheme.configurationHandler = { [unowned self] cell in 79 | cell.selectionStyle = .none 80 | cell.label.text = "First Input:" 81 | cell.input.text = self.firstFieldValue 82 | cell.input.keyboardType = .default // Since the other input cell changes this value, this cell must define what it wants to avoid reuse issues. 83 | cell.input.addTarget(self, action: #selector(AdvancedTableSchemeViewController.controlResigned(_:)), for: .editingDidEndOnExit) 84 | cell.input.addTarget(self, action: #selector(AdvancedTableSchemeViewController.textFieldUpdated(_:)), for: .editingDidEnd) 85 | } 86 | } 87 | 88 | secondFieldScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 89 | scheme.configurationHandler = { [unowned self] cell in 90 | cell.selectionStyle = .none 91 | cell.label.text = "Email:" 92 | cell.input.text = self.secondFieldValue 93 | cell.input.keyboardType = .emailAddress 94 | cell.input.addTarget(self, action: #selector(AdvancedTableSchemeViewController.controlResigned(_:)), for: .editingDidEndOnExit) 95 | cell.input.addTarget(self, action: #selector(AdvancedTableSchemeViewController.textFieldUpdated(_:)), for: .editingDidEnd) 96 | } 97 | } 98 | } 99 | 100 | builder.buildSchemeSet { builder in 101 | let headerView = HeaderFooterView() 102 | headerView.label.text = "Buttons! header view with a custom height" 103 | builder.headerView = headerView 104 | builder.headerViewHeight = .custom(100) 105 | 106 | let footerView = HeaderFooterView() 107 | footerView.label.text = "Footer view with custom height" 108 | builder.footerView = footerView 109 | builder.footerViewHeight = .custom(100) 110 | 111 | buttonsScheme = builder.buildScheme { (scheme: ArraySchemeBuilder) in 112 | scheme.objects = ["First", "Second", "Third", "Fourth"] 113 | 114 | scheme.configurationHandler = { [unowned self] cell, object in 115 | cell.selectionStyle = .none 116 | cell.textLabel?.text = object 117 | let button = UIButton(type: .infoDark) 118 | button.addTarget(self, action: #selector(AdvancedTableSchemeViewController.buttonPressed(_:)), for: .touchUpInside) 119 | cell.accessoryView = button 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | // MARK: Target-Action 127 | @objc func switcherUpdated(_ switcher: UISwitch) { 128 | if let scheme = tableScheme.scheme(containing: switcher) { 129 | if scheme === self.firstSwitchScheme { 130 | print("Toggle some feature, like allowing wifi!") 131 | self.wifiEnabled = switcher.isOn 132 | } else if scheme === self.secondSwitchScheme { 133 | print("Toggle some other feature, like bluetooth!") 134 | self.bluetoothEnabled = switcher.isOn 135 | } 136 | } 137 | } 138 | 139 | @objc func textFieldUpdated(_ textField: UITextField) { 140 | if let scheme = tableScheme.scheme(containing: textField) { 141 | if scheme === self.firstFieldScheme { 142 | print("Storing \"\(textField.text ?? "")\" for first text field!") 143 | self.firstFieldValue = textField.text ?? "" 144 | } else if scheme === self.secondFieldScheme { 145 | print("Storing \"\(textField.text ?? "")\" for the email!") 146 | self.secondFieldValue = textField.text ?? "" 147 | } 148 | } 149 | } 150 | 151 | @objc func buttonPressed(_ button: UIButton) { 152 | if let tuple = tableScheme.schemeWithIndex(containing: button) { 153 | if tuple.scheme === buttonsScheme { 154 | let object = buttonsScheme.objects[tuple.index] 155 | print("You pressed the button with object: \(object)") 156 | } 157 | } 158 | } 159 | 160 | @objc func controlResigned(_ control: UIResponder) { 161 | control.resignFirstResponder() 162 | } 163 | } 164 | 165 | class InputFieldCell: SchemeCell { 166 | let label = UILabel() 167 | let input = UITextField() 168 | 169 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 170 | label.translatesAutoresizingMaskIntoConstraints = false 171 | input.translatesAutoresizingMaskIntoConstraints = false 172 | super.init(style: style, reuseIdentifier: reuseIdentifier) 173 | contentView.addSubview(label) 174 | contentView.addSubview(input) 175 | setNeedsUpdateConstraints() 176 | } 177 | 178 | required init(coder aDecoder: NSCoder) { 179 | fatalError("init(coder:) has not been implemented") 180 | } 181 | 182 | override func updateConstraints() { 183 | let views = ["label": label, "input": input] 184 | contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-5-[label]-5-|", options: [], metrics: nil, views: views)) 185 | contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-5-[input]-5-|", options: [], metrics: nil, views: views)) 186 | contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-20-[label]", options: [], metrics: nil, views: views)) 187 | contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "[input(150)]-20-|", options: [], metrics: nil, views: views)) 188 | 189 | super.updateConstraints() 190 | } 191 | 192 | } 193 | 194 | class HeaderFooterView: UIView { 195 | 196 | lazy var label: UILabel = { 197 | let label = UILabel() 198 | label.font = .systemFont(ofSize: 20) 199 | label.numberOfLines = 0 200 | return label 201 | }() 202 | 203 | init() { 204 | super.init(frame: .zero) 205 | 206 | addSubview(label) 207 | 208 | label.translatesAutoresizingMaskIntoConstraints = false 209 | let views = ["label": label] 210 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-10-[label]-10-|", options: [], metrics: nil, views: views)) 211 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[label]-10-|", options: [], metrics: nil, views: views)) 212 | } 213 | 214 | required init(coder aDecoder: NSCoder) { 215 | fatalError("init(coder:) has not been implemented") 216 | } 217 | 218 | } 219 | 220 | -------------------------------------------------------------------------------- /TableSchemerExamples/AnimationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationsViewController.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 11/19/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import TableSchemer 10 | import UIKit 11 | 12 | class AnimationsViewController: UITableViewController { 13 | let ReuseIdentifier = "cell" 14 | var tableScheme: TableScheme! 15 | var toggleHiddenSchemeSetScheme: Scheme! 16 | var randomNumberScheme: Scheme! 17 | var hiddenSchemeSet: SchemeSet! 18 | var toggleHiddenSchemesScheme: Scheme! 19 | var hiddenScheme1: Scheme! 20 | var hiddenScheme2: Scheme! 21 | var randomizedArrayScheme: ArrayScheme! 22 | var toggledArrayScheme: ArrayScheme! 23 | 24 | var toggledArrayToggled = false 25 | var schemesHidden = true 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | title = "Schemes Animations and More" 31 | createTableScheme() 32 | } 33 | 34 | func createTableScheme() { 35 | tableScheme = TableScheme(tableView: tableView) { builder in 36 | builder.buildSchemeSet { builder in 37 | // Demonstrate cell reloading by using a random number in each configuration 38 | randomNumberScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 39 | scheme.configurationHandler = { [unowned self] cell in 40 | self.removeSubviewsInView(cell.contentView) 41 | cell.selectionStyle = .none 42 | let randomNumber = arc4random() % 100 43 | cell.textLabel?.text = "Random number: \(randomNumber)" 44 | } 45 | 46 | scheme.selectionHandler = { [unowned(unsafe) self] cell, scheme in 47 | self.tableScheme.reloadScheme(self.randomNumberScheme, in: self.tableView, with: .fade) 48 | } 49 | } 50 | 51 | self.toggleHiddenSchemeSetScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 52 | scheme.configurationHandler = { [unowned(unsafe) self] cell in 53 | self.removeSubviewsInView(cell.contentView) 54 | cell.textLabel?.text = nil 55 | cell.selectionStyle = .none 56 | let button = UIButton(frame: CGRect(x: 10, y: 0, width: 300, height: 44)) 57 | button.setTitle("Tap to toggle hidden scheme set", for: .normal) 58 | button.setTitleColor(UIColor.black, for: .normal) 59 | button.addTarget(self, action: #selector(AnimationsViewController.buttonPressed(_:)), for: .touchUpInside) 60 | cell.contentView.addSubview(button) 61 | } 62 | } 63 | } 64 | 65 | hiddenSchemeSet = builder.buildSchemeSet { builder in 66 | builder.headerText = "Hidden Sample" 67 | builder.hidden = true 68 | 69 | toggleHiddenSchemesScheme = builder.buildScheme { (scheme: BasicSchemeBuilder) in 70 | 71 | scheme.configurationHandler = { [unowned self] cell in 72 | self.removeSubviewsInView(cell.contentView) 73 | cell.textLabel?.text = nil 74 | cell.selectionStyle = .none 75 | let button = UIButton(frame: CGRect(x: 10, y: 0, width: 300, height: 44)) 76 | button.setTitle("Tap to toggle other schemes visibility", for: .normal) 77 | button.setTitleColor(UIColor.black, for: .normal) 78 | button.addTarget(self, action: #selector(AnimationsViewController.buttonPressed(_:)), for: .touchUpInside) 79 | cell.contentView.addSubview(button) 80 | } 81 | } 82 | 83 | hiddenScheme1 = builder.buildScheme { (scheme: BasicSchemeBuilder, hidden: inout Bool) in 84 | hidden = true 85 | 86 | scheme.configurationHandler = { [unowned self] cell in 87 | self.removeSubviewsInView(cell.contentView) 88 | cell.textLabel?.text = nil 89 | cell.selectionStyle = .none 90 | cell.textLabel?.text = "First" 91 | cell.accessoryView = nil 92 | } 93 | } 94 | 95 | hiddenScheme2 = builder.buildScheme { (scheme: BasicSchemeBuilder, hidden: inout Bool) in 96 | hidden = true 97 | 98 | scheme.configurationHandler = { [unowned self] cell in 99 | self.removeSubviewsInView(cell.contentView) 100 | cell.selectionStyle = .none 101 | cell.textLabel?.text = "Second" 102 | cell.accessoryView = nil 103 | } 104 | } 105 | } 106 | 107 | 108 | builder.buildSchemeSet { builder in 109 | builder.headerText = "Intrascheme Animations" 110 | 111 | /* 112 | The following two schemes are an example of explicitly giving the animations the table view 113 | should be performing. Note that all of the indexes used are relative to the objects 114 | positions inside of the scheme itself. The rest of the table view isn't a concern when creating 115 | these animations. 116 | 117 | Note that you still need to update the objects array, otherwise you'll get errors when the cells 118 | get configured. 119 | */ 120 | builder.buildScheme { (scheme: BasicSchemeBuilder) in 121 | scheme.configurationHandler = { [unowned self] cell in 122 | self.removeSubviewsInView(cell.contentView) 123 | cell.selectionStyle = .none 124 | cell.textLabel?.text = "Tap to toggle preset array" 125 | } 126 | 127 | scheme.selectionHandler = { [unowned self] cell, scheme in 128 | if !self.toggledArrayToggled { 129 | self.tableScheme.animateChangesToScheme(self.toggledArrayScheme, inTableView: self.tableView) { animator in 130 | animator.deleteObject(at: 1, with: .left) 131 | animator.deleteObject(at: 3, with: .right) 132 | animator.deleteObject(at: 5, with: .left) 133 | animator.deleteObject(at: 7, with: .right) 134 | animator.deleteObject(at: 8, with: .top) 135 | animator.moveObject(at: 4, to: 3) 136 | animator.moveObject(at: 6, to: 2) 137 | animator.insertObject(at: 4, with: .top) 138 | self.toggledArrayScheme.objects = [1,3,7,5,11] 139 | } 140 | } else { 141 | self.tableScheme.animateChangesToScheme(self.toggledArrayScheme, inTableView: self.tableView) { animator in 142 | animator.insertObject(at: 1, with: .left) 143 | animator.insertObject(at: 3, with: .right) 144 | animator.insertObject(at: 5, with: .left) 145 | animator.insertObject(at: 7, with: .right) 146 | animator.insertObject(at: 8, with: .top) 147 | animator.moveObject(at: 2, to: 6) 148 | animator.moveObject(at: 3, to: 4) 149 | animator.deleteObject(at: 4, with: .top) 150 | self.toggledArrayScheme.objects = [1,2,3,4,5,6,7,8,9] 151 | } 152 | } 153 | 154 | self.toggledArrayToggled = !self.toggledArrayToggled 155 | } 156 | } 157 | 158 | toggledArrayScheme = builder.buildScheme { (scheme: ArraySchemeBuilder) in 159 | scheme.objects = [1,2,3,4,5,6,7,8,9] 160 | 161 | scheme.configurationHandler = { [unowned self] cell, object in 162 | self.removeSubviewsInView(cell.contentView) 163 | cell.selectionStyle = .none 164 | cell.textLabel?.text = "\(object)" 165 | } 166 | } 167 | 168 | /* 169 | The following two schemes are an example of letting TableSchemer decide the animations that are needed. I personally 170 | like the way the Fade animation works for inferred animations like this, and generally recommend this method unless 171 | you care about the animations on a specific row. 172 | 173 | The one requirement to support inferred animations is that the scheme conform to InferrableRowAnimatableScheme. The builtin 174 | ArrayScheme conforms to it, so for most cases it should be free to use! 175 | */ 176 | builder.buildScheme { (scheme: BasicSchemeBuilder) in 177 | 178 | scheme.configurationHandler = { [unowned self] cell in 179 | self.removeSubviewsInView(cell.contentView) 180 | cell.selectionStyle = .none 181 | cell.textLabel?.text = "Tap to toggle random inferred array" 182 | } 183 | 184 | scheme.selectionHandler = { [unowned self] cell, object in 185 | self.tableScheme.animateChangesToScheme(self.randomizedArrayScheme, inTableView: self.tableView, withAnimation: .fade) { 186 | self.randomizedArrayScheme.objects = self.generateRandomizedArray() 187 | } 188 | } 189 | } 190 | 191 | randomizedArrayScheme = builder.buildScheme { (scheme: ArraySchemeBuilder) in 192 | scheme.objects = generateRandomizedArray() 193 | 194 | scheme.configurationHandler = { [unowned self] cell, object in 195 | self.removeSubviewsInView(cell.contentView) 196 | cell.selectionStyle = .none 197 | cell.textLabel?.text = "\(object)" 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | private func removeSubviewsInView(_ view: UIView) { 205 | for v in view.subviews { 206 | v.removeFromSuperview() 207 | } 208 | } 209 | 210 | private func generateRandomizedArray() -> [Int] { 211 | let itemCount = Int(arc4random() % 20) 212 | var items = [Int]() 213 | 214 | for i in 0.. Bool { 16 | window = UIWindow(frame: UIScreen.main.bounds) 17 | window!.rootViewController = UINavigationController(rootViewController: MasterViewController(style: .grouped)) 18 | window!.makeKeyAndVisible() 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TableSchemerExamples/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TableSchemerExamples/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TableSchemerExamples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarTintParameters 30 | 31 | UINavigationBar 32 | 33 | Style 34 | UIBarStyleDefault 35 | Translucent 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /TableSchemerExamples/MasterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/12/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import TableSchemer 10 | import UIKit 11 | 12 | class MasterViewController: UITableViewController { 13 | let ReuseIdentifier = "cell" 14 | var arrayObjects = ["Item 1", "Item 2", "A really long item to demonstrate height handling at its finest"] 15 | var tableScheme: TableScheme! 16 | var accordionSelection = 0 17 | var radioSelection = 0 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | title = "Sample Schemes" 23 | 24 | createTableScheme() 25 | 26 | tableView.rowHeight = 44.0 27 | } 28 | 29 | func createTableScheme() { 30 | tableScheme = TableScheme(tableView: tableView, allowReordering: true) { builder in 31 | builder.buildSchemeSet { builder in 32 | builder.buildScheme { (scheme: BasicSchemeBuilder) in 33 | 34 | scheme.configurationHandler = { cell in 35 | cell.textLabel?.text = "Tap here for animation examples." 36 | cell.accessoryType = .disclosureIndicator 37 | } 38 | 39 | scheme.selectionHandler = { [unowned self] cell, scheme in 40 | let animationController = AnimationsViewController(style: .grouped) 41 | self.navigationController!.pushViewController(animationController, animated: true) 42 | } 43 | } 44 | 45 | builder.buildScheme { (scheme: BasicSchemeBuilder) in 46 | scheme.configurationHandler = { cell in 47 | cell.textLabel?.text = "Tap here for an advanced example." 48 | cell.accessoryType = .disclosureIndicator 49 | } 50 | 51 | scheme.selectionHandler = { [unowned self] cell, scheme in 52 | let advancedController = AdvancedTableSchemeViewController(style: .grouped) 53 | self.navigationController!.pushViewController(advancedController, animated: true) 54 | self.tableView.deselectRow(at: self.tableView.indexPathForSelectedRow!, animated: true) 55 | } 56 | } 57 | } 58 | 59 | builder.buildSchemeSet { builder in 60 | builder.headerText = "Accordion Sample" 61 | 62 | builder.buildScheme { (scheme: AccordionSchemeBuilder) in 63 | scheme.expandedCellTypes = [UITableViewCell.Type](repeating: UITableViewCell.self, count: 3) 64 | scheme.accordionHeights = [.useTable, .custom(88.0)] // Demonstrating that if we don't have enough heights to cover all items, it defaults to .UseTable 65 | scheme.collapsedCellConfigurationHandler = { [unowned(unsafe) self] (cell) in // Be sure to use unowned(unsafe) references for the config/selection handlers 66 | _ = cell.textLabel?.text = "Selected Index: \(self.accordionSelection)" 67 | } 68 | 69 | scheme.collapsedCellSelectionHandler = { cell, scheme in 70 | print("Opening Accordion!") 71 | } 72 | 73 | scheme.expandedCellConfigurationHandler = { [unowned self] cell, index in 74 | cell.textLabel?.text = "Accordion Expanded Cell \(index + 1)" 75 | if index == self.accordionSelection { 76 | cell.accessoryType = .checkmark 77 | } else { 78 | cell.accessoryType = .none 79 | } 80 | } 81 | 82 | scheme.expandedCellSelectionHandler = { [unowned self] cell, scheme, selectedIndex in 83 | self.accordionSelection = selectedIndex 84 | } 85 | } 86 | } 87 | 88 | builder.buildSchemeSet { builder in 89 | builder.headerText = "Array Sample" 90 | 91 | builder.buildScheme { (scheme: ArraySchemeBuilder) in 92 | scheme.objects = arrayObjects 93 | 94 | scheme.heightHandler = { object in 95 | let rect = object.boundingRect(with: CGSize(width: 300, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: nil, context: nil) 96 | let height = CGFloat(ceilf(Float(rect.size.height)) + 28.0) 97 | return .custom(height) 98 | } 99 | 100 | scheme.configurationHandler = { cell, object in 101 | cell.textLabel?.text = object 102 | cell.textLabel?.numberOfLines = 0 103 | cell.textLabel?.preferredMaxLayoutWidth = 300 104 | cell.textLabel?.lineBreakMode = .byWordWrapping 105 | cell.textLabel?.invalidateIntrinsicContentSize() // For when this cell gets reused 106 | } 107 | 108 | scheme.selectionHandler = { cell, scheme, object in 109 | print("Selected object in ArrayScheme: \(object)") 110 | self.tableView.deselectRow(at: self.tableView.indexPathForSelectedRow!, animated: true) 111 | } 112 | 113 | scheme.reorderingHandler = { [unowned self] objects in 114 | print("Reordered objects in ArrayScheme: \(objects)") 115 | self.arrayObjects = objects 116 | } 117 | } 118 | } 119 | 120 | builder.buildSchemeSet { builder in 121 | builder.headerText = "Radio Sample" 122 | 123 | builder.buildScheme { (scheme: RadioSchemeBuilder) in 124 | scheme.expandedCellTypes = [UITableViewCell.Type](repeating: UITableViewCell.self, count: 5) 125 | 126 | scheme.configurationHandler = { cell, index in 127 | cell.textLabel?.text = "Radio Button \(index + 1)" 128 | } 129 | 130 | scheme.selectionHandler = { [unowned self] cell, scheme, index in 131 | print("You selected \(index)!") 132 | self.radioSelection = index 133 | self.tableView.deselectRow(at: self.tableView.indexPathForSelectedRow!, animated: true) 134 | } 135 | } 136 | } 137 | 138 | builder.buildSchemeSet { builder in 139 | builder.headerText = "Custom Appearance Radio Sample" 140 | 141 | builder.buildScheme { (scheme: RadioSchemeBuilder) in 142 | scheme.expandedCellTypes = [ColorizedTableViewCell.Type](repeating: ColorizedTableViewCell.self, count: 5) 143 | 144 | scheme.configurationHandler = { cell, index in 145 | cell.textLabel?.text = "Radio Button \(index + 1)" 146 | } 147 | 148 | scheme.selectionHandler = { [unowned self] cell, scheme, index in 149 | print("You selected \(index)!") 150 | self.radioSelection = index 151 | } 152 | 153 | scheme.stateHandler = { cell, _, _, selected in 154 | cell.backgroundColor = selected ? .green : .red 155 | } 156 | } 157 | } 158 | 159 | } 160 | } 161 | } 162 | 163 | fileprivate class ColorizedTableViewCell: UITableViewCell { 164 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 165 | super.init(style: style, reuseIdentifier: reuseIdentifier) 166 | textLabel?.backgroundColor = .clear 167 | } 168 | 169 | required init?(coder aDecoder: NSCoder) { 170 | super.init(coder: aDecoder) 171 | textLabel?.backgroundColor = .clear 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /TableSchemerTests/AccordionScheme_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccordionScheme_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/14/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import TableSchemer 12 | 13 | class AccordionScheme_Tests: XCTestCase { 14 | let ReuseIdentifier = "UITableViewCell" 15 | 16 | var subject: AccordionScheme! 17 | 18 | // MARK: Setup and Teardown 19 | override func tearDown() { 20 | subject = nil 21 | super.tearDown() 22 | } 23 | 24 | // MARK: Items 25 | func testItems_matchesReuseIdentifierCount() { 26 | configureSubjectWithConfigurationHandler() 27 | XCTAssert(subject.numberOfItems == 3) 28 | } 29 | 30 | // MARK: Configuring Cell 31 | func testConfigureCell_whenUnexpanded_callsConfigurationBlockWithCell() { 32 | var passedCell: UITableViewCell? 33 | configureSubjectWithConfigurationHandler({(cell) in 34 | passedCell = cell 35 | }) 36 | 37 | let configureCell = UITableViewCell() 38 | subject.configureCell(configureCell, withRelativeIndex: 0) 39 | 40 | XCTAssert(passedCell === configureCell) 41 | } 42 | 43 | func testConfigureCell_whenExpanded_callsAccordionConfigurationBlockWithCell() { 44 | var passedCell1: UITableViewCell? 45 | var passedCell2: UITableViewCell? 46 | var passedCell3: UITableViewCell? 47 | 48 | configureSubjectWithConfigurationHandler({(cell) in 49 | }, accordionConfigurationHandler: {(cell, index) in 50 | if index == 0 { 51 | passedCell1 = cell 52 | } else if index == 1 { 53 | passedCell2 = cell 54 | } else if index == 2 { 55 | passedCell3 = cell 56 | } 57 | }) 58 | 59 | let tableView = UITableView() 60 | let configureCell1 = UITableViewCell() 61 | let configureCell2 = UITableViewCell() 62 | let configureCell3 = UITableViewCell() 63 | 64 | subject.selectCell(configureCell1, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 65 | 66 | subject.configureCell(configureCell1, withRelativeIndex: 0) 67 | subject.configureCell(configureCell2, withRelativeIndex: 1) 68 | subject.configureCell(configureCell3, withRelativeIndex: 2) 69 | 70 | XCTAssert(passedCell1 === configureCell1) 71 | XCTAssert(passedCell2 === configureCell2) 72 | XCTAssert(passedCell3 === configureCell3) 73 | } 74 | 75 | // MARK: Select Cell 76 | func testSelectCell_whenUnexpanded_callsSelectBlock() { 77 | var passedCell: UITableViewCell? 78 | var passedScheme: AccordionScheme? 79 | 80 | configureSubjectWithConfigurationHandler() 81 | subject.selectionHandler = {(cell, scheme) in 82 | passedCell = cell 83 | passedScheme = scheme as? AccordionScheme 84 | } 85 | 86 | let tableView = UITableView() 87 | let cell = UITableViewCell() 88 | 89 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 90 | 91 | XCTAssert(passedCell === cell) 92 | XCTAssert(passedScheme === subject) 93 | } 94 | 95 | func testSelectCell_whenExpanded_callsAccordionSelectBlock() { 96 | var passedCell: UITableViewCell? 97 | var passedScheme: AccordionScheme? 98 | var passedIndex: Int? 99 | 100 | configureSubjectWithConfigurationHandler() 101 | subject.accordionSelectionHandler = {(cell, scheme, index) in 102 | passedCell = cell 103 | passedScheme = scheme 104 | passedIndex = index 105 | } 106 | 107 | let tableView = UITableView() 108 | let cell = UITableViewCell() 109 | 110 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 111 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 1) 112 | 113 | XCTAssert(passedCell === cell) 114 | XCTAssert(passedScheme === subject) 115 | XCTAssert(passedIndex == 1) 116 | } 117 | 118 | func testSelectCell_whenUnexpanded_expandsCell() { 119 | configureSubjectWithConfigurationHandler() 120 | 121 | let tableView = UITableView() 122 | let cell = UITableViewCell() 123 | 124 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 125 | 126 | XCTAssertEqual(subject.numberOfCells, 3) 127 | } 128 | 129 | func testSelectCell_whenExpanded_unexpandsCell() { 130 | configureSubjectWithConfigurationHandler() 131 | 132 | let tableView = UITableView() 133 | let cell = UITableViewCell() 134 | 135 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 136 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 137 | 138 | XCTAssertEqual(subject.numberOfCells, 1) 139 | } 140 | 141 | func testSelectCell_whenUnexpanded_whenFirstRowIsSelected_animatesNewCellsIn() { 142 | configureSubjectWithConfigurationHandler() 143 | 144 | let tableView = RecordingTableView() 145 | let cell = UITableViewCell() 146 | 147 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 148 | 149 | XCTAssertEqual(1, tableView.callsToBeginUpdates) 150 | XCTAssertEqual(1, tableView.callsToEndUpdates) 151 | 152 | XCTAssertEqual(1, tableView.callsToInsertRows.count) 153 | if tableView.callsToInsertRows.count > 0 { 154 | XCTAssertEqual([IndexPath(row: 1, section: 0), IndexPath(row: 2, section: 0)], tableView.callsToInsertRows[0].indexPaths) 155 | XCTAssertEqual(.fade, tableView.callsToInsertRows[0].animation) 156 | } 157 | 158 | XCTAssertEqual(1, tableView.callsToReloadRows.count) 159 | if tableView.callsToReloadRows.count > 0 { 160 | XCTAssertEqual([IndexPath(row: 0, section: 0)], tableView.callsToReloadRows[0].indexPaths) 161 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 162 | } 163 | } 164 | 165 | func testSelectCell_whenUnexpanded_whenLastRowIsSelected_animatesNewCellsIn() { 166 | configureSubjectWithConfigurationHandler() 167 | subject.selectedIndex = 2 168 | 169 | let tableView = RecordingTableView() 170 | let cell = UITableViewCell() 171 | 172 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 173 | 174 | XCTAssertEqual(1, tableView.callsToBeginUpdates) 175 | XCTAssertEqual(1, tableView.callsToEndUpdates) 176 | 177 | XCTAssertEqual(1, tableView.callsToInsertRows.count) 178 | if tableView.callsToInsertRows.count > 0 { 179 | XCTAssertEqual([IndexPath(row: 0, section: 0), IndexPath(row: 1, section: 0)], tableView.callsToInsertRows[0].indexPaths) 180 | XCTAssertEqual(.fade, tableView.callsToInsertRows[0].animation) 181 | } 182 | 183 | XCTAssertEqual(1, tableView.callsToReloadRows.count) 184 | if tableView.callsToReloadRows.count > 0 { 185 | XCTAssertEqual([IndexPath(row: 0, section: 0)], tableView.callsToReloadRows[0].indexPaths) 186 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 187 | } 188 | } 189 | 190 | func testSelectCell_whenUnexpanded_whenMiddleRowIsSelected_animatesNewCellsIn() { 191 | configureSubjectWithConfigurationHandler() 192 | subject.selectedIndex = 1 193 | 194 | let tableView = RecordingTableView() 195 | let cell = UITableViewCell() 196 | 197 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 1, withRelativeIndex: 0) 198 | 199 | XCTAssertEqual(1, tableView.callsToBeginUpdates) 200 | XCTAssertEqual(1, tableView.callsToEndUpdates) 201 | 202 | XCTAssertEqual(2, tableView.callsToInsertRows.count) 203 | if tableView.callsToInsertRows.count > 1 { 204 | XCTAssertEqual([IndexPath(row: 1, section: 0)], tableView.callsToInsertRows[0].indexPaths) 205 | XCTAssertEqual(.fade, tableView.callsToInsertRows[0].animation) 206 | 207 | XCTAssertEqual([IndexPath(row: 3, section: 0)], tableView.callsToInsertRows[1].indexPaths) 208 | XCTAssertEqual(.fade, tableView.callsToInsertRows[1].animation) 209 | } 210 | 211 | XCTAssertEqual(1, tableView.callsToReloadRows.count) 212 | if tableView.callsToReloadRows.count > 0 { 213 | XCTAssertEqual([IndexPath(row: 1, section: 0)], tableView.callsToReloadRows[0].indexPaths) 214 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 215 | } 216 | } 217 | 218 | func testSelectCell_whenExpanded_whenFirstRowIsSelected_animatesOldCellsOut() { 219 | configureSubjectWithConfigurationHandler() 220 | 221 | let tableView = RecordingTableView() 222 | let cell = UITableViewCell() 223 | 224 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 225 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 226 | 227 | XCTAssertEqual(2, tableView.callsToBeginUpdates) 228 | XCTAssertEqual(2, tableView.callsToEndUpdates) 229 | 230 | XCTAssertEqual(1, tableView.callsToDeleteRows.count) 231 | if tableView.callsToDeleteRows.count > 0 { 232 | XCTAssertEqual([IndexPath(row: 1, section: 0), IndexPath(row: 2, section: 0)], tableView.callsToDeleteRows[0].indexPaths) 233 | XCTAssertEqual(.fade, tableView.callsToDeleteRows[0].animation) 234 | } 235 | 236 | XCTAssertEqual(2, tableView.callsToReloadRows.count) 237 | if tableView.callsToReloadRows.count > 1 { 238 | XCTAssertEqual([IndexPath(row: 0, section: 0)], tableView.callsToReloadRows[0].indexPaths) 239 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 240 | 241 | XCTAssertEqual([IndexPath(row: 0, section: 0)], tableView.callsToReloadRows[1].indexPaths) 242 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[1].animation) 243 | } 244 | } 245 | 246 | func testSelectCell_whenExpanded_whenLastRowIsSelected_animatesOldCellsOut() { 247 | configureSubjectWithConfigurationHandler() 248 | 249 | let tableView = RecordingTableView() 250 | let cell = UITableViewCell() 251 | 252 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 253 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 2) 254 | 255 | XCTAssertEqual(2, tableView.callsToBeginUpdates) 256 | XCTAssertEqual(2, tableView.callsToEndUpdates) 257 | 258 | XCTAssertEqual(1, tableView.callsToDeleteRows.count) 259 | if tableView.callsToDeleteRows.count > 0 { 260 | XCTAssertEqual([IndexPath(row: 0, section: 0), IndexPath(row: 1, section: 0)], tableView.callsToDeleteRows[0].indexPaths) 261 | XCTAssertEqual(.fade, tableView.callsToDeleteRows[0].animation) 262 | } 263 | 264 | XCTAssertEqual(2, tableView.callsToReloadRows.count) 265 | if tableView.callsToReloadRows.count > 1 { 266 | XCTAssertEqual([IndexPath(row: 0, section: 0)], tableView.callsToReloadRows[0].indexPaths) 267 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 268 | 269 | XCTAssertEqual([IndexPath(row: 2, section: 0)], tableView.callsToReloadRows[1].indexPaths) 270 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[1].animation) 271 | } 272 | } 273 | 274 | func testSelectCell_whenExpanded_whenMiddleRowIsSelected_animatesOldCellsOut() { 275 | configureSubjectWithConfigurationHandler() 276 | 277 | let tableView = RecordingTableView() 278 | let cell = UITableViewCell() 279 | 280 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 1, withRelativeIndex: 0) 281 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 1, withRelativeIndex: 1) 282 | 283 | XCTAssertEqual(2, tableView.callsToBeginUpdates) 284 | XCTAssertEqual(2, tableView.callsToEndUpdates) 285 | 286 | XCTAssertEqual(2, tableView.callsToDeleteRows.count) 287 | if tableView.callsToDeleteRows.count > 1 { 288 | XCTAssertEqual([IndexPath(row: 1, section: 0)], tableView.callsToDeleteRows[0].indexPaths) 289 | XCTAssertEqual(.fade, tableView.callsToDeleteRows[0].animation) 290 | 291 | XCTAssertEqual([IndexPath(row: 3, section: 0)], tableView.callsToDeleteRows[1].indexPaths) 292 | XCTAssertEqual(.fade, tableView.callsToDeleteRows[1].animation) 293 | } 294 | 295 | XCTAssertEqual(2, tableView.callsToReloadRows.count) 296 | if tableView.callsToReloadRows.count > 1 { 297 | XCTAssertEqual([IndexPath(row: 1, section: 0)], tableView.callsToReloadRows[0].indexPaths) 298 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[0].animation) 299 | 300 | XCTAssertEqual([IndexPath(row: 2, section: 0)], tableView.callsToReloadRows[1].indexPaths) 301 | XCTAssertEqual(.automatic, tableView.callsToReloadRows[1].animation) 302 | } 303 | } 304 | 305 | // MARK: Number of Cells 306 | func testNumberOfCells_whenUnexpanded_is1() { 307 | configureSubjectWithConfigurationHandler() 308 | XCTAssertTrue(subject.numberOfCells == 1) 309 | } 310 | 311 | func testNumberOfCells_whenExpanded_isNumberOfAccordionItems() { 312 | configureSubjectWithConfigurationHandler() 313 | 314 | let tableView = UITableView() 315 | let cell = UITableViewCell() 316 | 317 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 318 | 319 | XCTAssert(subject.numberOfCells == 3) 320 | } 321 | 322 | // MARK: Reuse Identifier For Relative Index 323 | func testReuseIdentifierForRelativeIndex_whenUnexpanded_isUnexpandedReuseIdentifier() { 324 | configureSubjectWithConfigurationHandler() 325 | XCTAssertEqual(subject.reuseIdentifier(forRelativeIndex:0), ReuseIdentifier) 326 | } 327 | 328 | func testReuseIdentifierForRelativeIndex_whenExpanded_isCorrectExpandedReuseIdentifier() { 329 | configureSubjectWithConfigurationHandler() 330 | 331 | let tableView = UITableView() 332 | let cell = UITableViewCell() 333 | 334 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 335 | 336 | XCTAssertEqual(subject.reuseIdentifier(forRelativeIndex:1), "TestCell") 337 | } 338 | 339 | // MARK: Height For Relative Index 340 | func testHeightForRelativeIndex_usesDefinedHeight() { 341 | configureSubjectWithConfigurationHandler() 342 | subject.height = .custom(83.0) 343 | XCTAssertEqual(subject.height(forRelativeIndex:0), RowHeight.custom(83.0)) 344 | } 345 | 346 | func testHeightForRelativeIndex_defaultsToUseTableHeight() { 347 | configureSubjectWithConfigurationHandler() 348 | XCTAssertEqual(subject.height(forRelativeIndex:0), RowHeight.useTable) 349 | } 350 | 351 | func testHeightForRelativeIndex_whenExpanded_equalsAccordionHeights() { 352 | configureSubjectWithConfigurationHandler() 353 | subject.accordionHeights = [.custom(25.0), .custom(29.0)] 354 | 355 | let tableView = UITableView() 356 | let cell = UITableViewCell() 357 | 358 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 359 | 360 | XCTAssertEqual(subject.height(forRelativeIndex: 0), RowHeight.custom(25.0)) 361 | XCTAssertEqual(subject.height(forRelativeIndex: 1), RowHeight.custom(29.0)) 362 | XCTAssertEqual(subject.height(forRelativeIndex: 2), RowHeight.useTable) 363 | } 364 | 365 | // MARK: Test Configuration 366 | func configureSubjectWithConfigurationHandler(_ configurationHandler: @escaping BasicScheme.ConfigurationHandler = {(cell) in }, accordionConfigurationHandler: @escaping AccordionScheme.AccordionConfigurationHandler = {(cell, index) in }) { 367 | let expandedCells = [UITableViewCell.self, TestCell.self, UITableViewCell.self] 368 | subject = AccordionScheme(expandedCellTypes: expandedCells, collapsedCellConfigurationHandler: configurationHandler, expandedCellConfigurationHandler: accordionConfigurationHandler) 369 | } 370 | } 371 | 372 | class TestCell: UITableViewCell { } 373 | -------------------------------------------------------------------------------- /TableSchemerTests/ArrayScheme_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayScheme_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/14/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import TableSchemer 12 | 13 | class ArrayScheme_Tests: XCTestCase { 14 | let ReuseIdentifier = "UITableViewCell" 15 | var subject: ArrayScheme! 16 | 17 | // MARK: Setup and Teardown 18 | override func tearDown() { 19 | subject = nil 20 | super.tearDown() 21 | } 22 | 23 | // MARK: Abstract Method Overrides 24 | func testConfigureCell_callsConfigurationBlockWithCellAndObject() { 25 | var passedCell1: UITableViewCell? 26 | var passedCell2: UITableViewCell? 27 | 28 | configureSubjectWithObjects(["One", "Two"], configurationHandler: {(cell, object) in 29 | if object == "One" { 30 | passedCell1 = cell 31 | } else if object == "Two" { 32 | passedCell2 = cell 33 | } 34 | }) 35 | 36 | let configureCell1 = UITableViewCell() 37 | let configureCell2 = UITableViewCell() 38 | 39 | subject.configureCell(configureCell1, withRelativeIndex: 0) 40 | subject.configureCell(configureCell2, withRelativeIndex: 1) 41 | 42 | XCTAssert(passedCell1 === configureCell1) 43 | XCTAssert(passedCell2 === configureCell2) 44 | } 45 | 46 | func testSelectCell_callsSelectBlockWithCellAndSelfAndObject() { 47 | var passedCell: UITableViewCell? 48 | var passedScheme: ArrayScheme? 49 | var passedObject: String? 50 | 51 | let string1 = "One" 52 | let string2 = "Two" 53 | 54 | configureSubjectWithObjects([string1, string2], selectionHandler: { (cell, scheme, object) in 55 | passedCell = cell 56 | passedScheme = scheme 57 | passedObject = object 58 | }) 59 | 60 | let tableView = UITableView() 61 | let cell = UITableViewCell() 62 | 63 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 64 | 65 | XCTAssert(passedCell === cell) 66 | XCTAssert(passedScheme === subject) 67 | XCTAssert(passedObject == string1) 68 | } 69 | 70 | func testNumberOfCells_matchesObjectCount() { 71 | configureSubjectWithObjects(["One", "Two"]) 72 | XCTAssert(subject.numberOfCells == 2) 73 | } 74 | 75 | func testReuseIdentifierForRelativeIndex_isReuseIdentifier() { 76 | configureSubjectWithObjects() 77 | XCTAssertEqual(subject.reuseIdentifier(forRelativeIndex:0), ReuseIdentifier) 78 | } 79 | 80 | func testHeightForRelativeIndex_usesCallbackHeight() { 81 | let string1 = "One" 82 | let string2 = "Two" 83 | 84 | configureSubjectWithObjects([string1, string2]) 85 | subject.heightHandler = {(object) in 86 | if object == string1 { 87 | return .custom(44.0) 88 | } else if object == string2 { 89 | return .custom(80.0) 90 | } 91 | 92 | return .useTable 93 | } 94 | 95 | XCTAssertEqual(subject.height(forRelativeIndex: 0), RowHeight.custom(44)) 96 | XCTAssertEqual(subject.height(forRelativeIndex: 1), RowHeight.custom(80.0)) 97 | } 98 | 99 | // MARK: InferrableRowAnimatableScheme 100 | 101 | func testRowIdentifiers_equalsObjects() { 102 | let string1 = "One" 103 | let string2 = "Two" 104 | 105 | configureSubjectWithObjects([string1, string2]) 106 | 107 | XCTAssert(subject.rowIdentifiers == [string1, string2]) 108 | } 109 | 110 | // MARK: Test Configuration 111 | func configureSubjectWithObjects(_ objects: [String] = [], configurationHandler: @escaping ArrayScheme.ConfigurationHandler = {(cell, object) in}, selectionHandler: @escaping ArrayScheme.SelectionHandler = {(cell, scheme, object) in}) { 112 | subject = ArrayScheme(objects: objects, configurationHandler: configurationHandler) 113 | subject.selectionHandler = selectionHandler 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /TableSchemerTests/BasicScheme_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicScheme_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/13/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import TableSchemer 12 | 13 | class BasicScheme_Tests: XCTestCase { 14 | let ReuseIdentifier = "UITableViewCell" 15 | var subject: BasicScheme! 16 | 17 | // MARK: Setup and Teardown 18 | override func tearDown() { 19 | subject = nil 20 | super.tearDown() 21 | } 22 | 23 | // MARK: Instantiation 24 | func testInitDefaultsRowHeightToUseTableView() { 25 | configureSubjectWithHandler() 26 | XCTAssert(subject.height == RowHeight.useTable) 27 | } 28 | 29 | // MARK: Scheme Abstract Method Overrides 30 | func testConfigureCell_callsConfigurationBlockWithCell() { 31 | var passedCell: UITableViewCell? 32 | configureSubjectWithHandler { (cell) in 33 | passedCell = cell 34 | } 35 | 36 | let configureCell = UITableViewCell() 37 | subject.configureCell(configureCell, withRelativeIndex: 0) 38 | XCTAssert(passedCell === configureCell) 39 | } 40 | 41 | func testSelectCell_callsSelectBlockWithCellAndSelf() { 42 | configureSubjectWithHandler() 43 | var passedCell: UITableViewCell? 44 | var passedScheme: BasicScheme? 45 | subject.selectionHandler = {(cell, scheme) in 46 | passedCell = cell 47 | passedScheme = scheme 48 | } 49 | 50 | let tableView = UITableView() 51 | let cell = UITableViewCell() 52 | 53 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 0) 54 | 55 | XCTAssert(passedCell === cell) 56 | XCTAssert(passedScheme === subject) 57 | } 58 | 59 | func testReuseIdentifierForRelativeIndex_isReuseIdentifier() { 60 | configureSubjectWithHandler() 61 | XCTAssert(subject.reuseIdentifier(forRelativeIndex:0) == ReuseIdentifier) 62 | } 63 | 64 | func testHeightForRelativeIndex_usesDefinedHeight() { 65 | configureSubjectWithHandler() 66 | subject.height = .custom(83.0) 67 | XCTAssert(subject.height(forRelativeIndex: 0) == .custom(83.0)) 68 | } 69 | 70 | func testHeightForRelativeIndex_defaultsToUseTableHeight() { 71 | configureSubjectWithHandler() 72 | XCTAssert(subject.height(forRelativeIndex: 0) == .useTable) 73 | } 74 | 75 | // MARK: Test Configuration 76 | func configureSubjectWithHandler(_ handler: @escaping BasicScheme.ConfigurationHandler = {(cell) in }) { 77 | subject = BasicScheme(configurationHandler: handler) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /TableSchemerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.Weebly.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /TableSchemerTests/RadioScheme_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioScheme_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/14/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import TableSchemer 12 | 13 | class RadioScheme_Tests: XCTestCase { 14 | var subject: RadioScheme! 15 | 16 | // MARK: Setup and Teardown 17 | override func tearDown() { 18 | subject = nil 19 | super.tearDown() 20 | } 21 | 22 | // MARK: Configuring Cell 23 | func testConfigureCell_configuresCellWithCellAndRelativeIndex() { 24 | var passedCell: UITableViewCell? 25 | var passedIndex: Int? 26 | 27 | configureSubjectWithConfigurationHandler({ (cell, index) in 28 | passedCell = cell 29 | passedIndex = index 30 | }) 31 | 32 | let cell = UITableViewCell() 33 | 34 | subject.configureCell(cell, withRelativeIndex: 1) 35 | 36 | XCTAssert(passedCell === cell) 37 | XCTAssertEqual(passedIndex!, 1) 38 | } 39 | 40 | func testConfigureCell_whenSelected_setsCheckmarkAccessory() { 41 | configureSubjectWithConfigurationHandler() 42 | subject.selectedIndex = 1 43 | 44 | let cell = UITableViewCell() 45 | subject.configureCell(cell, withRelativeIndex: 1) 46 | 47 | XCTAssertEqual(cell.accessoryType, UITableViewCell.AccessoryType.checkmark) 48 | } 49 | 50 | func testConfigureCell_whenNotSelected_setsAccessoryToNone() { 51 | configureSubjectWithConfigurationHandler() 52 | subject.selectedIndex = 1 53 | 54 | let cell = UITableViewCell() 55 | cell.accessoryType = .checkmark 56 | subject.configureCell(cell, withRelativeIndex: 0) 57 | 58 | XCTAssertEqual(cell.accessoryType, UITableViewCell.AccessoryType.none) 59 | } 60 | 61 | func testConfigureCell_whenSelected_withCustomStateHandler_usesCustomStateHandler() { 62 | configureSubjectWithConfigurationHandler() 63 | subject.selectedIndex = 1 64 | 65 | let testColor = UIColor.red 66 | subject.stateHandler = { cell, _, _, selected in 67 | XCTAssertTrue(selected) 68 | cell.backgroundColor = testColor 69 | } 70 | 71 | let cell = UITableViewCell() 72 | subject.configureCell(cell, withRelativeIndex: 1) 73 | 74 | XCTAssertEqual(testColor, cell.backgroundColor) 75 | } 76 | 77 | func testConfigureCell_whenNotSelected_withCustomStateHandler_usesCustomStateHandler() { 78 | configureSubjectWithConfigurationHandler() 79 | subject.selectedIndex = 1 80 | 81 | let testColor = UIColor.red 82 | subject.stateHandler = { cell, _, _, selected in 83 | XCTAssertFalse(selected) 84 | cell.backgroundColor = testColor 85 | } 86 | 87 | let cell = UITableViewCell() 88 | subject.configureCell(cell, withRelativeIndex: 0) 89 | 90 | XCTAssertEqual(testColor, cell.backgroundColor) 91 | } 92 | 93 | // MARK: Selecing Cell 94 | func testSelectCell_updatesSelectedIndex() { 95 | configureSubjectWithConfigurationHandler() 96 | 97 | let tableView = UITableView() 98 | let cell = UITableViewCell() 99 | 100 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 1) 101 | 102 | XCTAssertEqual(subject.selectedIndex, 1) 103 | } 104 | 105 | func testSelectCell_callsSelectionHandler() { 106 | var passedCell: UITableViewCell? 107 | var passedScheme: RadioScheme? 108 | var passedIndex: Int? 109 | var selectedIndexAtCalling: Int? 110 | 111 | configureSubjectWithConfigurationHandler(selectionHandler: {(cell, scheme, index) in 112 | passedCell = cell 113 | passedScheme = scheme 114 | passedIndex = index 115 | selectedIndexAtCalling = scheme.selectedIndex 116 | }) 117 | 118 | let tableView = UITableView() 119 | let cell = UITableViewCell() 120 | 121 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 0, withRelativeIndex: 1) 122 | 123 | XCTAssert(passedCell === cell) 124 | XCTAssert(passedScheme === subject) 125 | XCTAssertEqual(passedIndex!, 1) 126 | XCTAssertEqual(selectedIndexAtCalling!, 0) 127 | } 128 | 129 | func testSelectCell_updatesPreviouslySelectedCellAccessoryType() { 130 | configureSubjectWithConfigurationHandler() 131 | 132 | let tableView = RecordingTableView() 133 | let oldCell = UITableViewCell() 134 | oldCell.accessoryType = .checkmark 135 | 136 | let indexPath = IndexPath(row: 3, section: 0) 137 | tableView.cellOverrides[indexPath] = oldCell 138 | 139 | let cell = UITableViewCell() 140 | 141 | subject.selectCell(cell, inTableView: tableView, inSection: 0, havingRowsBeforeScheme: 3, withRelativeIndex: 1) 142 | 143 | XCTAssertEqual(oldCell.accessoryType, UITableViewCell.AccessoryType.none) 144 | XCTAssertEqual(cell.accessoryType, UITableViewCell.AccessoryType.checkmark) 145 | } 146 | 147 | // MARK: Number of Cells 148 | func testNumberOfCells_isNumberOfReuseIdentifiers() { 149 | configureSubjectWithConfigurationHandler() 150 | 151 | XCTAssertEqual(subject.numberOfCells, 2) 152 | } 153 | 154 | // MARK: Reuse Identifier for Relative Index 155 | func testReuseIdentifierForRelativeIndex_matchesReuseIdentifiers() { 156 | configureSubjectWithConfigurationHandler() 157 | 158 | XCTAssertEqual(subject.reuseIdentifier(forRelativeIndex: 0), "UITableViewCell") 159 | XCTAssertEqual(subject.reuseIdentifier(forRelativeIndex: 1), "UITableViewCell") 160 | } 161 | 162 | // MARK: Height For Relative Index 163 | func testReuseIdentifiersForRelativeIndex_matchesReuseIdentifiers() { 164 | configureSubjectWithConfigurationHandler() 165 | subject.heights = [.custom(22.0), .custom(44.0)] 166 | 167 | XCTAssertEqual(subject.height(forRelativeIndex: 0), RowHeight.custom(22.0)) 168 | XCTAssertEqual(subject.height(forRelativeIndex: 1), RowHeight.custom(44.0)) 169 | } 170 | 171 | func testHeightForRelativeIndex_defaultsToUseTableHeight() { 172 | configureSubjectWithConfigurationHandler() 173 | 174 | XCTAssertEqual(subject.height(forRelativeIndex: 0), RowHeight.useTable) 175 | } 176 | 177 | // MARK: Test Configuration 178 | func configureSubjectWithConfigurationHandler(_ configurationHandler: @escaping RadioScheme.ConfigurationHandler = {(cell, index) in }, selectionHandler: @escaping RadioScheme.SelectionHandler = {(cell, scheme, index) in}) { 179 | subject = RadioScheme(expandedCellTypes: [UITableViewCell.self, UITableViewCell.self], configurationHandler: configurationHandler) 180 | subject.selectionHandler = selectionHandler 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /TableSchemerTests/SchemeSetBuilder_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeSetBuilder_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/15/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TableSchemer 11 | 12 | class SchemeSetBuilder_Tests: XCTestCase { 13 | // MARK: Creating Scheme Sets 14 | func testCreateSchemeSet_setsHeaderTExt() { 15 | let subject = SchemeSetBuilder() 16 | subject.headerText = "Foo Bar" 17 | let schemeSet = subject.createSchemeSet() 18 | XCTAssertEqual(schemeSet.schemeSet.headerText, "Foo Bar") 19 | } 20 | 21 | func testCreateSchemeSet_setsBuiltSchemeSets() { 22 | let subject = SchemeSetBuilder() 23 | let scheme1 = subject.buildScheme {(builder: BasicSchemeBuilder) in 24 | builder.configurationHandler = {(cell: UITableViewCell) in} 25 | } 26 | let scheme2 = subject.buildScheme {(builder: BasicSchemeBuilder) in 27 | builder.configurationHandler = {(cell: UITableViewCell) in} 28 | } 29 | 30 | let schemeSet = subject.createSchemeSet() 31 | XCTAssert(schemeSet.schemeSet.schemes[0] === scheme1) 32 | XCTAssert(schemeSet.schemeSet.schemes[1] === scheme2) 33 | } 34 | 35 | func testCreateSchemeSet_setsFooterText() { 36 | let subject = SchemeSetBuilder() 37 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 38 | builder.configurationHandler = {(cell: UITableViewCell) in} 39 | } 40 | 41 | subject.footerText = "Foo bar" 42 | 43 | let schemeSet = subject.createSchemeSet() 44 | XCTAssert(schemeSet.schemeSet.footerText == "Foo bar") 45 | } 46 | 47 | func testCreateScheme_setsHeaderView() { 48 | let subject = SchemeSetBuilder() 49 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 50 | builder.configurationHandler = {(cell: UITableViewCell) in} 51 | } 52 | 53 | let view = UIView() 54 | subject.headerView = view 55 | 56 | let schemeSet = subject.createSchemeSet() 57 | XCTAssert(schemeSet.schemeSet.headerView === view) 58 | } 59 | 60 | func testCreateScheme_setsHeaderViewHeight() { 61 | let subject = SchemeSetBuilder() 62 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 63 | builder.configurationHandler = {(cell: UITableViewCell) in} 64 | } 65 | 66 | subject.headerViewHeight = .custom(42) 67 | 68 | let schemeSet = subject.createSchemeSet() 69 | XCTAssertEqual(schemeSet.schemeSet.headerViewHeight, .custom(42)) 70 | } 71 | 72 | func testCreateScheme_setsFooterView() { 73 | let subject = SchemeSetBuilder() 74 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 75 | builder.configurationHandler = {(cell: UITableViewCell) in} 76 | } 77 | 78 | let view = UIView() 79 | subject.footerView = view 80 | 81 | let schemeSet = subject.createSchemeSet() 82 | XCTAssert(schemeSet.schemeSet.footerView === view) 83 | } 84 | 85 | func testCreateScheme_setsFooterViewHeight() { 86 | let subject = SchemeSetBuilder() 87 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 88 | builder.configurationHandler = {(cell: UITableViewCell) in} 89 | } 90 | 91 | subject.footerViewHeight = .custom(42) 92 | 93 | let schemeSet = subject.createSchemeSet() 94 | XCTAssertEqual(schemeSet.schemeSet.footerViewHeight, .custom(42)) 95 | } 96 | 97 | func testCreateSchemeSet_setsHidden() { 98 | let subject = SchemeSetBuilder() 99 | _ = subject.buildScheme {(builder: BasicSchemeBuilder) in 100 | builder.configurationHandler = {(cell: UITableViewCell) in} 101 | } 102 | 103 | subject.hidden = true 104 | 105 | let schemeSet = subject.createSchemeSet() 106 | XCTAssertTrue(schemeSet.hidden) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /TableSchemerTests/SchemeSet_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeSet_Tests.swift 3 | // TableSchemer 4 | // 5 | // Created by James Richard on 6/15/14. 6 | // Copyright (c) 2014 Weebly. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TableSchemer 11 | 12 | class SchemeSet_Tests: XCTestCase { 13 | // MARK: Initializers 14 | func testInitWithNameAndSchemes_setsSchemes() { 15 | let schemes: [Scheme] = [TestableScheme()] 16 | let subject = SchemeSet(schemes: schemes, headerText: "Foo Bar") 17 | XCTAssertTrue(subject.schemes.isEqualToSchemes(schemes)) 18 | } 19 | 20 | func testInitWithSchemes_setsSchemes() { 21 | let schemes: [Scheme] = [TestableScheme()] 22 | let subject = SchemeSet(schemes: schemes) 23 | XCTAssertTrue(subject.schemes.isEqualToSchemes(schemes as [Scheme])) 24 | } 25 | 26 | func testInitWithNameAndSchemes_setsName() { 27 | let subject: SchemeSet = SchemeSet(schemes: [TestableScheme()], headerText: "Foo Bar") 28 | XCTAssert(subject.headerText == "Foo Bar") 29 | } 30 | 31 | func testInitWithNameFooterTextAndSchemes_setsFooterText() { 32 | let subject: SchemeSet = SchemeSet(schemes: [TestableScheme()], headerText: "Foo Bar", footerText: "Buzz") 33 | XCTAssert(subject.footerText == "Buzz") 34 | } 35 | 36 | func testInitWithFooterTextAndSchemes_setsFooterText() { 37 | let subject: SchemeSet = SchemeSet(schemes: [TestableScheme()], footerText: "Buzz") 38 | XCTAssert(subject.footerText == "Buzz") 39 | } 40 | 41 | func testInitWithHeaderViewAndSchemes_setsHeaderView() { 42 | let view = UIView() 43 | let subject = SchemeSet(schemes: [TestableScheme()], headerView: view) 44 | XCTAssert(subject.headerView === view) 45 | } 46 | 47 | func testInitWithHeaderViewHeightAndSchemes_setsHeaderViewHeight() { 48 | let subject = SchemeSet(schemes: [TestableScheme()], headerViewHeight: .custom(54)) 49 | XCTAssertEqual(subject.headerViewHeight, .custom(54)) 50 | } 51 | 52 | func testInitWithFooterViewAndSchemes_setsFooterView() { 53 | let view = UIView() 54 | let subject = SchemeSet(schemes: [TestableScheme()], footerView: view) 55 | XCTAssert(subject.footerView === view) 56 | } 57 | 58 | func testInitWithFooterViewHeightAndSchemes_setsFooterViewHeight() { 59 | let subject = SchemeSet(schemes: [TestableScheme()], footerViewHeight: .custom(54)) 60 | XCTAssertEqual(subject.footerViewHeight, .custom(54)) 61 | } 62 | 63 | // MARK: Subscript Support 64 | func testSubscript_accessesSchemes() { 65 | let schemes: [Scheme] = [TestableScheme()] 66 | let subject = SchemeSet(schemes: schemes) 67 | 68 | XCTAssert(subject[0] === schemes[0]) 69 | } 70 | 71 | // MARK: Count 72 | func testCount_whenOneScheme_is1() { 73 | let subject = SchemeSet(schemes: [TestableScheme()]) 74 | 75 | XCTAssertEqual(subject.count, 1) 76 | } 77 | 78 | func testCount_whenTwoSchemes_is2() { 79 | let subject = SchemeSet(schemes: [TestableScheme(), TestableScheme()]) 80 | 81 | XCTAssertEqual(subject.count, 2) 82 | } 83 | 84 | // MARK: Visibility 85 | func testVisibleSchemes_onlyIncludeVisibleSchemes() { 86 | let scheme1 = TestableScheme() 87 | let scheme2 = TestableScheme() 88 | let schemes: [Scheme] = [scheme1, scheme2] 89 | let subject = SchemeSet(schemes: schemes) 90 | subject.attributedSchemes[0].hidden = true 91 | XCTAssertTrue(subject.visibleSchemes.isEqualToSchemes([scheme2] as [Scheme])) 92 | } 93 | 94 | } 95 | 96 | extension Sequence where Self.Iterator.Element == Scheme { 97 | func isEqualToSchemes(_ schemes: SchemeSequenceType) -> Bool where SchemeSequenceType.Iterator.Element == Scheme { 98 | var gen = makeIterator() 99 | for scheme in schemes { 100 | guard let comp = gen.next() else { 101 | return false 102 | } 103 | 104 | if comp !== scheme { 105 | return false 106 | } 107 | } 108 | 109 | return true 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /TableSchemerTests/TableSchemerTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | --------------------------------------------------------------------------------