diff --git a/Example/Sources/TableViewController.swift b/Example/Sources/TableViewController.swift index 285271e..0c843e7 100644 --- a/Example/Sources/TableViewController.swift +++ b/Example/Sources/TableViewController.swift @@ -42,11 +42,26 @@ final class TableViewController: UITableViewController { cell.accessibilityIdentifier = "\(indexPath.section), \(indexPath.row)" return cell } - + // 3. create data source provider dataSourceProvider = DataSourceProvider(dataSource: dataSource, cellFactory: factory, supplementaryFactory: factory) - // 4. set data source + // 4. Optional - create if neccessary a datasourceEditingController to enable the editing functionality on the tableView + let tableDataSourceEditingController = TableDataSourceEditingController( + canEditConfigurator: { (indexPath, tableView) -> Bool in + return indexPath.row % 2 == 0 + }, + commitEditingStyle:{ (tableView, editingStyle, indexPath) in + if editingStyle == .delete { + if let _ = self.dataSourceProvider?.dataSource.remove(at: indexPath) { + tableView.deleteRows(at: [indexPath], with: .automatic) + } + } + }) + + dataSourceProvider?.tableEditingController = tableDataSourceEditingController + + // 5. set data source tableView.dataSource = dataSourceProvider?.tableViewDataSource } diff --git a/JSQDataSourcesKit.xcodeproj/project.pbxproj b/JSQDataSourcesKit.xcodeproj/project.pbxproj index 66d0c24..ddac0e6 100644 --- a/JSQDataSourcesKit.xcodeproj/project.pbxproj +++ b/JSQDataSourcesKit.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1D68ECA31DFFEF4600A1AFB7 /* TableDataSourceEditingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD7AC5C1D86C4BC00B676A6 /* TableDataSourceEditingController.swift */; }; + 1DD7AC5D1D86C4BC00B676A6 /* TableDataSourceEditingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD7AC5C1D86C4BC00B676A6 /* TableDataSourceEditingController.swift */; }; 881A92DB1CB881550080BC5C /* FetchedResultsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DCAD2C1CB87CB400C018AF /* FetchedResultsDelegate.swift */; }; 881A92DD1CB881550080BC5C /* JSQDataSourcesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 88DCAD2F1CB87CB400C018AF /* JSQDataSourcesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 881A92E11CB881550080BC5C /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DCAD331CB87CB400C018AF /* Section.swift */; }; @@ -84,6 +86,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1DD7AC5C1D86C4BC00B676A6 /* TableDataSourceEditingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableDataSourceEditingController.swift; sourceTree = ""; }; 881A92CC1CB881270080BC5C /* JSQDataSourcesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JSQDataSourcesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 882CA4531D1D7D48006112B9 /* BridgedFetchedResultsDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BridgedFetchedResultsDelegate.swift; sourceTree = ""; }; 882CA4571D1D82E1006112B9 /* TestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCase.swift; sourceTree = ""; }; @@ -178,6 +181,7 @@ 88DCAD2C1CB87CB400C018AF /* FetchedResultsDelegate.swift */, 88DCAD2D1CB87CB400C018AF /* Info.plist */, 88DCAD2F1CB87CB400C018AF /* JSQDataSourcesKit.h */, + 1DD7AC5C1D86C4BC00B676A6 /* TableDataSourceEditingController.swift */, 88DCAD331CB87CB400C018AF /* Section.swift */, 88DCAD351CB87CB400C018AF /* TitledSupplementaryView.swift */, 88DCAD371CB87CB400C018AF /* TitledSupplementaryViewFactory.swift */, @@ -388,6 +392,7 @@ 88B80CD91CBBF62A00EDF9D5 /* DataSourceProvider.swift in Sources */, 882CA4551D1D7DE1006112B9 /* BridgedFetchedResultsDelegate.swift in Sources */, 882CA4561D1D7DE1006112B9 /* DataSource.swift in Sources */, + 1D68ECA31DFFEF4600A1AFB7 /* TableDataSourceEditingController.swift in Sources */, 881A92DB1CB881550080BC5C /* FetchedResultsDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -403,6 +408,7 @@ 88DCAD761CB87CC000C018AF /* TitledSupplementaryView.swift in Sources */, 88DCAD741CB87CC000C018AF /* Section.swift in Sources */, 88B80CD81CBBF62A00EDF9D5 /* DataSourceProvider.swift in Sources */, + 1DD7AC5D1D86C4BC00B676A6 /* TableDataSourceEditingController.swift in Sources */, 888799491D0E2D1700BBCCBC /* DataSource.swift in Sources */, 88DCAD781CB87CC000C018AF /* TitledSupplementaryViewFactory.swift in Sources */, ); diff --git a/Source/BridgedDataSource.swift b/Source/BridgedDataSource.swift index 0a24696..1db4549 100644 --- a/Source/BridgedDataSource.swift +++ b/Source/BridgedDataSource.swift @@ -30,6 +30,9 @@ internal typealias TableCellForRowAtIndexPathHandler = (UITableView, IndexPath) internal typealias TableTitleForHeaderInSectionHandler = (Int) -> String? internal typealias TableTitleForFooterInSectionHandler = (Int) -> String? +internal typealias TableCanEditHandler = (UITableView, IndexPath) -> Bool +internal typealias TableCommitEditingStyleHandler = (UITableView, UITableViewCellEditingStyle, IndexPath) -> Void + /* This class is responsible for implementing the `UICollectionViewDataSource` and `UITableViewDataSource` protocols. @@ -46,6 +49,9 @@ internal typealias TableTitleForFooterInSectionHandler = (Int) -> String? var tableCellForRowAtIndexPath: TableCellForRowAtIndexPathHandler? var tableTitleForHeaderInSection: TableTitleForHeaderInSectionHandler? var tableTitleForFooterInSection: TableTitleForFooterInSectionHandler? + + var tableCanEditRow: TableCanEditHandler? + var tableCommitEditingStyleForRow: TableCommitEditingStyleHandler? init(numberOfSections: @escaping NumberOfSectionsHandler, numberOfItemsInSection: @escaping NumberOfItemsInSectionHandler) { @@ -103,4 +109,15 @@ extension BridgedDataSource: UITableViewDataSource { } return nil } + + @objc func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + if let closure = tableCanEditRow { + return closure(tableView,indexPath) + } + return false + } + + @objc func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + tableCommitEditingStyleForRow?(tableView,editingStyle,indexPath) + } } diff --git a/Source/DataSource.swift b/Source/DataSource.swift index 8233789..593491c 100644 --- a/Source/DataSource.swift +++ b/Source/DataSource.swift @@ -82,7 +82,7 @@ extension DataSourceProtocol { - returns: The item specified by indexPath, or `nil`. */ - func item(atIndexPath indexPath: IndexPath) -> Item? { + public func item(atIndexPath indexPath: IndexPath) -> Item? { return item(atRow: indexPath.row, inSection: indexPath.section) } } @@ -159,7 +159,20 @@ public struct DataSource: DataSourceProtocol { guard section < sections.count else { return nil } return sections[section].footerTitle } - + + /** + Removes an item at the specified index path. + + - parameter indexPath: The index path specifying the location of the item. + - returns: The item at `indexPath`, or `nil` if it does not exist. + */ + @discardableResult + public mutating func remove(at indexPath: IndexPath) -> S.Item? { + guard item(atIndexPath: indexPath) != nil else { return nil } + let section = indexPath.section + let row = indexPath.row + return sections[section].items.remove(at: row) + } // MARK: Subscripts diff --git a/Source/DataSourceProvider.swift b/Source/DataSourceProvider.swift index 1622d60..520d54a 100644 --- a/Source/DataSourceProvider.swift +++ b/Source/DataSourceProvider.swift @@ -37,7 +37,8 @@ where CellFactory.Item == DataSource.Item, SupplementaryFactory.Item == DataSour public let supplementaryFactory: SupplementaryFactory fileprivate var bridgedDataSource: BridgedDataSource? - + + fileprivate var _tableEditingController: TableDataSourceEditingController? // MARK: Initialization @@ -61,7 +62,6 @@ where CellFactory.Item == DataSource.Item, SupplementaryFactory.Item == DataSour } } - public extension DataSourceProvider where CellFactory.View: UITableViewCell { // MARK: UITableViewDataSource @@ -73,7 +73,16 @@ public extension DataSourceProvider where CellFactory.View: UITableViewCell { } return bridgedDataSource! } - + + public var tableEditingController: TableDataSourceEditingController? { + set { + _tableEditingController = newValue + } + get { + return _tableEditingController + } + } + private func tableViewBridgedDataSource() -> BridgedDataSource { let dataSource = BridgedDataSource( numberOfSections: { [unowned self] () -> Int in @@ -95,6 +104,15 @@ public extension DataSourceProvider where CellFactory.View: UITableViewCell { dataSource.tableTitleForFooterInSection = { [unowned self] (section) -> String? in return self.dataSource.footerTitle(inSection: section) } + + dataSource.tableCanEditRow = { [unowned self] (tableView, indexPath) -> Bool in + guard let editDataSource = self.tableEditingController else { return false } + return editDataSource.canEditRowAt(indexPath: indexPath, in: tableView) + } + + dataSource.tableCommitEditingStyleForRow = { [unowned self] (tableView, editingStyle,indexPath) in + self.tableEditingController?.commitEditStyleForRow(in: tableView, editingStyle: editingStyle, at: indexPath) + } return dataSource } diff --git a/Source/TableDataSourceEditingController.swift b/Source/TableDataSourceEditingController.swift new file mode 100644 index 0000000..2477085 --- /dev/null +++ b/Source/TableDataSourceEditingController.swift @@ -0,0 +1,45 @@ +// +// Created by Jesse Squires +// http://www.jessesquires.com +// +// +// Documentation +// http://jessesquires.com/JSQDataSourcesKit +// +// +// GitHub +// https://github.com/jessesquires/JSQDataSourcesKit +// +// +// License +// Copyright © 2015 Jesse Squires +// Released under an MIT license: http://opensource.org/licenses/MIT +// + +import Foundation +import UIKit + +public struct TableDataSourceEditingController { + + public typealias CanEditRowConfigurator = (IndexPath, UITableView) -> Bool + public typealias CommitEditingStyleConfigurator = (UITableView, UITableViewCellEditingStyle, IndexPath) -> Void + + public let canEditConfigurator: CanEditRowConfigurator + public let commitEditingStyle: CommitEditingStyleConfigurator + + public init(canEditConfigurator: @escaping CanEditRowConfigurator, + commitEditingStyle: @escaping CommitEditingStyleConfigurator) { + + self.canEditConfigurator = canEditConfigurator + self.commitEditingStyle = commitEditingStyle + } + + public func canEditRowAt(indexPath: IndexPath, in tableView: UITableView) -> Bool { + return canEditConfigurator(indexPath, tableView) + } + + public func commitEditStyleForRow(in tableView: UITableView, editingStyle: UITableViewCellEditingStyle, at indexPath: IndexPath) { + return commitEditingStyle(tableView, editingStyle, indexPath) + } + +} diff --git a/Tests/DataSourceProviderTests.swift b/Tests/DataSourceProviderTests.swift index 2d1e861..b7a264e 100644 --- a/Tests/DataSourceProviderTests.swift +++ b/Tests/DataSourceProviderTests.swift @@ -369,4 +369,93 @@ final class DataSourceProviderTests: TestCase { } } + func test_thatDataSourceProvider_forTableView_returnsExpectedData_afterRemovingRowFromTableView() { + // GIVEN: a single section with data items + let expectedModel = FakeViewModel() + let expectedIndexPath = IndexPath(row: 2, section: 0) + + let section0 = Section(items: FakeViewModel(), FakeViewModel(), expectedModel, FakeViewModel(), FakeViewModel(), + headerTitle: "Header", + footerTitle: "Footer") + let dataSource = DataSource([section0]) + + let oldItemForExpectedIndexPath = dataSource.item(atRow: expectedIndexPath.row, inSection: expectedIndexPath.section) + let oldCount = dataSource.numberOfItems(inSection: expectedIndexPath.section) + + let factoryExpectation = expectation(description: #function) + tableView.dequeueCellExpectation = expectation(description: dequeueCellExpectationName + #function) + + typealias TableCellFactory = ViewFactory + var dataSourceProvider: DataSourceProvider>, TableCellFactory, TableCellFactory>! + + // GIVEN: a cell factory + let factory = ViewFactory(reuseIdentifier: cellReuseId) { (cell, model: FakeViewModel?, type, tableView, indexPath) -> FakeTableCell in + + XCTAssertEqual(cell.reuseIdentifier!, self.cellReuseId, "Dequeued cell should have expected identifier") + XCTAssertEqual(tableView, self.tableView, "TableView should equal the tableView for the data source") + + factoryExpectation.fulfill() + return cell + } + + //GIVEN: a data source editing controller + let tableDataSourceEditingController = TableDataSourceEditingController( + canEditConfigurator: { (indexPath, tableView) -> Bool in + return indexPath == expectedIndexPath + }, + commitEditingStyle:{ (tableView, editingStyle, indexPath) in + if editingStyle == .delete { + if let _ = dataSourceProvider.dataSource.remove(at: indexPath) { + tableView.deleteRows(at: [indexPath], with: .automatic) + } + } + }) + + // GIVEN: a data source provider + dataSourceProvider = DataSourceProvider(dataSource: dataSource, cellFactory: factory, supplementaryFactory: factory) + dataSourceProvider.tableEditingController = tableDataSourceEditingController + + let tableViewDataSource = dataSourceProvider.tableViewDataSource + tableView.dataSource = tableViewDataSource + + // WHEN: we call the table view data source methods + let canEditRow = tableViewDataSource.tableView?(tableView, canEditRowAt: expectedIndexPath) + tableViewDataSource.tableView?(tableView, commit: .delete, forRowAt: expectedIndexPath) + let newItemForExpectedIndexPath = dataSourceProvider.dataSource.item(atRow: expectedIndexPath.row, inSection: expectedIndexPath.section) + let newCount = dataSourceProvider.dataSource.numberOfItems(inSection: expectedIndexPath.section) + + let numSections = tableViewDataSource.numberOfSections?(in: tableView) + let cell = tableViewDataSource.tableView(tableView, cellForRowAt: expectedIndexPath) + let header = tableViewDataSource.tableView?(tableView, titleForHeaderInSection: 0) + let footer = tableViewDataSource.tableView?(tableView, titleForFooterInSection: 0) + + // THEN: we receive the expected return values + XCTAssertNotNil(canEditRow, "canEditRow should not be nil") + XCTAssert(canEditRow!, "expectedIndexpath should be able to be removed from tableview") + + XCTAssertNotEqual(oldCount, newCount, "Number of items for \(expectedIndexPath.section) should not be equal after removing the expected row") + XCTAssertEqual(newCount, oldCount - 1, "Number of items for \(expectedIndexPath.section) should be less by one") + XCTAssertNotEqual(newItemForExpectedIndexPath, oldItemForExpectedIndexPath, "old item at row \(expectedIndexPath.row), section \(expectedIndexPath.section) shouldn't exist any more") + + XCTAssertNotNil(numSections, "Number of sections should not be nil") + XCTAssertEqual(numSections!, dataSourceProvider.dataSource.sections.count, "Data source should return expected number of sections") + + XCTAssertNotNil(cell.reuseIdentifier, "Cell reuse identifier should not be nil") + XCTAssertEqual(cell.reuseIdentifier!, cellReuseId, "Data source should return cells with the expected identifier") + + XCTAssertNotNil(header, "Header should not be nil") + XCTAssertNotNil(section0.headerTitle, "Section 0 header title should not be nil") + XCTAssertEqual(header!, section0.headerTitle!, "Data source should return expected header title") + + XCTAssertNotNil(footer, "Footer should not be nil") + XCTAssertNotNil(section0.footerTitle, "Section 0 footer title should not be nil") + XCTAssertEqual(footer!, section0.footerTitle!, "Data source should return expected footer title") + + // THEN: the tableView calls `dequeueReusableCellWithIdentifier` + // THEN: the cell factory calls its `ConfigurationHandler` + waitForExpectations(timeout: defaultTimeout, handler: { (error) -> Void in + XCTAssertNil(error, "Expectations should not error") + }) + } + } diff --git a/Tests/DataSourceTests.swift b/Tests/DataSourceTests.swift index f6875dd..16b22e1 100644 --- a/Tests/DataSourceTests.swift +++ b/Tests/DataSourceTests.swift @@ -201,7 +201,7 @@ final class DataSourceTests: XCTestCase { let ip = IndexPath(item: 2, section: 2) let item = dataSource[ip] - // THEN: we receive the exepected data + // THEN: we receive the expected data XCTAssertEqual(item, model) } @@ -219,4 +219,21 @@ final class DataSourceTests: XCTestCase { // THEN: the item is replaced XCTAssertEqual(dataSource[ip], item) } + + func test_thatDataSource_removesExpectedData_atIndexPath() { + // GIVEN: a data source + let sectionA = Section(items: FakeViewModel(), FakeViewModel(), headerTitle: "Header") + let sectionB = Section(items: FakeViewModel(), FakeViewModel(), footerTitle: "Footer") + var dataSource = DataSource(sections: sectionA, sectionB) + + // WHEN: we set an item at a specific index path + let ip = IndexPath(item: 1, section: 0) + let itemToRemove = dataSource.item(atIndexPath: ip) + + // THEN: Check if an item exists at the specified indexPath .Then check if the removedItem is the expected item + XCTAssertNotNil(itemToRemove) + + let removedItem = dataSource.remove(at: ip) + XCTAssertEqual(removedItem, itemToRemove) + } }