Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TableView and CollectionView protocol #139

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ReactiveLists.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
357B96E9201956760000443F /* CollectionViewHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 357B96E8201956760000443F /* CollectionViewHeaderView.xib */; };
357B96EA2019599C0000443F /* CollectionViewHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 357B96E8201956760000443F /* CollectionViewHeaderView.xib */; };
7C24B1408A8B3A147C254BCA /* Pods_ReactiveLists.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 978763204EC113AFD1F7EB54 /* Pods_ReactiveLists.framework */; };
A860FAF3214076B000EFEC1B /* TableViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A860FAF2214076B000EFEC1B /* TableViewProtocol.swift */; };
C11B2E5FBD1253C9FFA431A1 /* Pods_ReactiveListsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 932473A2DAECE0F923C4B570 /* Pods_ReactiveListsTests.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -135,6 +136,7 @@
932473A2DAECE0F923C4B570 /* Pods_ReactiveListsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactiveListsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
950EFA9B77AA7C8F0696C57B /* Pods-ReactiveLists.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveLists.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists.debug.xcconfig"; sourceTree = "<group>"; };
978763204EC113AFD1F7EB54 /* Pods_ReactiveLists.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactiveLists.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A860FAF2214076B000EFEC1B /* TableViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewProtocol.swift; sourceTree = "<group>"; };
C984EFFC6B6F170CB9AD8DD3 /* Pods-ReactiveListsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveListsTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests.release.xcconfig"; sourceTree = "<group>"; };
DFAFA51D8FEF2F413206A2CF /* Pods-ReactiveLists-ReactiveListsExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveLists-ReactiveListsExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -269,6 +271,7 @@
258E31D21F0D8F3100D6F324 /* SupplementaryViewInfo.swift */,
258E31AE1F0D8D9C00D6F324 /* TableViewDriver.swift */,
258E31AF1F0D8D9C00D6F324 /* TableViewModel.swift */,
A860FAF2214076B000EFEC1B /* TableViewProtocol.swift */,
32124A712019312200EE12FC /* Typealiases.swift */,
32753F8C201BB8310084DCB1 /* UICollectionView+Extensions.swift */,
32753F8E201BB8470084DCB1 /* UITableView+Extensions.swift */,
Expand Down Expand Up @@ -578,6 +581,7 @@
buildActionMask = 2147483647;
files = (
3203532D201BF5FB0024D6CC /* CellContainerViewProtocol.swift in Sources */,
A860FAF3214076B000EFEC1B /* TableViewProtocol.swift in Sources */,
2541B73D1F29A13B002C3090 /* Diffing.swift in Sources */,
258E31B31F0D8D9C00D6F324 /* TableViewDriver.swift in Sources */,
32753F8D201BB8310084DCB1 /* UICollectionView+Extensions.swift in Sources */,
Expand Down
37 changes: 34 additions & 3 deletions Sources/TableViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ open class TableViewDriver: NSObject {
}

/// The table view to which the `TableViewModel` is rendered.
public let tableView: UITableView
let tableView: TableView
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed okay to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tried it internally, but I think in most cases, you'll have another pointer to this view anyway.


private var _tableViewModel: TableViewModel?

Expand Down Expand Up @@ -72,6 +72,8 @@ open class TableViewDriver: NSObject {

private let _automaticDiffingEnabled: Bool

private let _registerViews: (TableViewModel) -> Void
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to avoid associatedtype issues with CellContainerViewProtocol.


/// Initializes a data source that drives a `UITableView` based on a `TableViewModel`.
///
/// - Parameters:
Expand All @@ -82,15 +84,44 @@ open class TableViewDriver: NSObject {
/// - automaticDiffingEnabled: defines whether or not this data source updates the table
/// view automatically when cells/sections are moved/inserted/deleted.
/// Defaults to `true`.
public init(
public convenience init(
tableView: UITableView,
tableViewModel: TableViewModel? = nil,
shouldDeselectUponSelection: Bool = true,
automaticDiffingEnabled: Bool = true) {
self.init(
tableView: tableView,
registerViews: tableView.registerViews(for:),
tableViewModel: tableViewModel,
shouldDeselectUponSelection: shouldDeselectUponSelection,
automaticDiffingEnabled: automaticDiffingEnabled
)
}

/// Initializes a data source that drives a `TableView` based on a `TableViewModel`.
/// Externally, this is a `UITableView`. Internally, this can be a mock.
///
/// - Parameters:
/// - tableView: the table view to which this data source will render its view models.
/// - registerViews: a closure that registers the views for a given `TableViewModel`
/// - tableViewModel: the view model that describes the initial state of this table view.
/// - shouldDeselectUponSelection: indicates if selected cells should immediately be
/// deselected. Defaults to `true`.
/// - automaticDiffingEnabled: defines whether or not this data source updates the table
/// view automatically when cells/sections are moved/inserted/deleted.
/// Defaults to `true`.
init(
tableView: TableView,
registerViews: @escaping (TableViewModel) -> Void,
tableViewModel: TableViewModel? = nil,
shouldDeselectUponSelection: Bool = true,
automaticDiffingEnabled: Bool = true
) {
self._tableViewModel = tableViewModel
self.tableView = tableView
self._automaticDiffingEnabled = automaticDiffingEnabled
self._shouldDeselectUponSelection = shouldDeselectUponSelection
self._registerViews = registerViews
super.init()
tableView.dataSource = self
tableView.delegate = self
Expand Down Expand Up @@ -163,7 +194,7 @@ open class TableViewDriver: NSObject {
}

if let newModel = newModel {
self.tableView.registerViews(for: newModel)
self._registerViews(newModel)
}

let previousStateNilOrEmpty = (oldModel == nil || oldModel!.isEmpty)
Expand Down
78 changes: 78 additions & 0 deletions Sources/TableViewProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// PlanGrid
// https://www.plangrid.com
// https://medium.com/plangrid-technology
//
// Documentation
// https://plangrid.github.io/ReactiveLists
//
// GitHub
// https://github.com/plangrid/ReactiveLists
//
// License
// Copyright © 2018-present PlanGrid, Inc.
// Released under an MIT license: https://opensource.org/licenses/MIT
//

import DifferenceKit

/// Protocol that allows ReactiveLists to use a UITableView without knowing about the concrete type
/// This is useful for testing, and it's a step toward having one
protocol TableView: class {

// MARK: UITableView methods

var indexPathsForVisibleRows: [IndexPath]? { get }
func beginUpdates()
func endUpdates()
func reloadData()
func cellForRow(at indexPath: IndexPath) -> UITableViewCell?
var dataSource: UITableViewDataSource? { get set }
var delegate: UITableViewDelegate? { get set }
func headerView(forSection section: Int) -> UITableViewHeaderFooterView?
func footerView(forSection section: Int) -> UITableViewHeaderFooterView?

// MARK: DifferenceKit UITableView extensions

//swiftlint:disable:next function_parameter_count
func reload<C>(
using stagedChangeset: StagedChangeset<C>,
deleteSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
insertSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
reloadSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
deleteRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
insertRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
reloadRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
interrupt: ((Changeset<C>) -> Bool)?,
setData: (C) -> Void
)
}

extension TableView {

//swiftlint:disable:next function_parameter_count
func reload<C>(
using stagedChangeset: StagedChangeset<C>,
deleteSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
insertSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
reloadSectionsAnimation: @autoclosure () -> UITableViewRowAnimation,
deleteRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
insertRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
reloadRowsAnimation: @autoclosure () -> UITableViewRowAnimation,
setData: (C) -> Void
) {
self.reload(
using: stagedChangeset,
deleteSectionsAnimation: deleteSectionsAnimation,
insertSectionsAnimation: insertRowsAnimation,
reloadSectionsAnimation: reloadSectionsAnimation,
deleteRowsAnimation: deleteRowsAnimation,
insertRowsAnimation: insertRowsAnimation,
reloadRowsAnimation: reloadRowsAnimation,
interrupt: nil,
setData: setData
)
}
}

extension UITableView: TableView {}
2 changes: 1 addition & 1 deletion Sources/UITableView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import UIKit

extension UITableView {
extension TableView where Self: CellContainerViewProtocol, Self.CellType: UITableViewCell {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protocol extension with generic constraints!


func configuredCell(for model: TableCellViewModel, at indexPath: IndexPath) -> UITableViewCell {
let cell = self.dequeueReusableCellFor(identifier: model.registrationInfo.reuseIdentifier, indexPath: indexPath)
Expand Down
15 changes: 9 additions & 6 deletions Tests/TableView/TableViewDiffingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// Released under an MIT license: https://opensource.org/licenses/MIT
//

import DifferenceKit
@testable import ReactiveLists
import XCTest

Expand Down Expand Up @@ -51,8 +52,9 @@ final class TableViewDiffingTests: XCTestCase {

self.tableViewDataSource.tableViewModel = updatedModel

XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths.count, 1)
XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths[0].indexPaths, [IndexPath(row: 0, section: 0)])
XCTAssertEqual(self.mockTableView.callsToReloadViaDiff.count, 1)
let insertedRows = self.mockTableView.callsToReloadViaDiff[0].elementInserted
XCTAssertEqual(insertedRows, [ElementPath(element: 0, section: 0)])
}

func testChangingRowsWithEmptyModles() {
Expand All @@ -68,7 +70,7 @@ final class TableViewDiffingTests: XCTestCase {

self.tableViewDataSource.tableViewModel = updatedModel

XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths.count, 0)
XCTAssertEqual(self.mockTableView.callsToReloadViaDiff.count, 0)
XCTAssertEqual(self.mockTableView.callsToReloadData, 3)
}

Expand Down Expand Up @@ -101,8 +103,9 @@ final class TableViewDiffingTests: XCTestCase {

self.tableViewDataSource.tableViewModel = updatedModel

XCTAssertEqual(self.mockTableView.callsToDeleteSections.count, 1)
XCTAssertEqual(self.mockTableView.callsToDeleteSections[0].sections, IndexSet(integer: 0))
let deletedSections = self.mockTableView.callsToReloadViaDiff[0].sectionDeleted
XCTAssertEqual(deletedSections.count, 1)
XCTAssertEqual(deletedSections, [0])
}

func testChangingSectionsThatAreEmpty() {
Expand All @@ -128,7 +131,7 @@ final class TableViewDiffingTests: XCTestCase {

self.tableViewDataSource.tableViewModel = updatedModel

XCTAssertEqual(self.mockTableView.callsToDeleteSections.count, 0)
XCTAssertEqual(self.mockTableView.callsToReloadViaDiff.count, 0)
XCTAssertEqual(self.mockTableView.callsToReloadData, 3)
}
}
Expand Down
Loading