Skip to content

Commit

Permalink
Auto cell registration + unify table and collection views
Browse files Browse the repository at this point in the history
Rather than copy-pasta table view code for collection view, let's unify them.

This introduces `CellParentViewProtocol` to reduce duplication in auto-registering cells. It unifies the APIs of tables and collections.

On top of this, we add `ReusableCellProtocol` and `ReusableSupplementaryViewProtocol` to unify our cell view models and supplementary view models.

Now there's a single API for registering cells automatically.

All work based on:
https://github.com/jessesquires/JSQDataSourcesKit/blob/develop/Source/ReusableViewConfig.swift

Almost completes #18, starts initial work on #46
  • Loading branch information
jessesquires committed Jan 28, 2018
1 parent 664292c commit ad69c94
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 60 deletions.
4 changes: 4 additions & 0 deletions ReactiveLists.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
25B1B0B920195F1C0036545F /* CollectionViewDriverDiffingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25B1B0B720195F160036545F /* CollectionViewDriverDiffingTests.swift */; };
25B1B0BA201A53CF0036545F /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32124A712019312200EE12FC /* Typealiases.swift */; };
3203532B201BE9E90024D6CC /* ToolTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32035329201BE99F0024D6CC /* ToolTableViewCell.swift */; };
3203532D201BF5FB0024D6CC /* CellParentViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3203532C201BF5FB0024D6CC /* CellParentViewProtocol.swift */; };
32753F8D201BB8310084DCB1 /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32753F8C201BB8310084DCB1 /* UICollectionView+Extensions.swift */; };
32753F8F201BB8470084DCB1 /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32753F8E201BB8470084DCB1 /* UITableView+Extensions.swift */; };
32E7A1F8201BADE800B90EBC /* ViewRegistrationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E7A1F7201BADE800B90EBC /* ViewRegistrationInfo.swift */; };
Expand Down Expand Up @@ -118,6 +119,7 @@
276442FEC917A7E0F32CE5B4 /* Pods-ReactiveLists-ReactiveListsExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveLists-ReactiveListsExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample.debug.xcconfig"; sourceTree = "<group>"; };
2F35530D29268B112F99A187 /* Pods_ReactiveLists_ReactiveListsExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactiveLists_ReactiveListsExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
32035329201BE99F0024D6CC /* ToolTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTableViewCell.swift; sourceTree = "<group>"; };
3203532C201BF5FB0024D6CC /* CellParentViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellParentViewProtocol.swift; sourceTree = "<group>"; };
32124A712019312200EE12FC /* Typealiases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typealiases.swift; sourceTree = "<group>"; };
32753F8C201BB8310084DCB1 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = "<group>"; };
32753F8E201BB8470084DCB1 /* UITableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -262,6 +264,7 @@
isa = PBXGroup;
children = (
258E31D11F0D8F3100D6F324 /* AccessibilityFormats.swift */,
3203532C201BF5FB0024D6CC /* CellParentViewProtocol.swift */,
258E31AC1F0D8D9C00D6F324 /* CollectionViewDriver.swift */,
258E31AD1F0D8D9C00D6F324 /* CollectionViewModel.swift */,
2541B73C1F29A13B002C3090 /* Diffing.swift */,
Expand Down Expand Up @@ -627,6 +630,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3203532D201BF5FB0024D6CC /* CellParentViewProtocol.swift in Sources */,
2541B73D1F29A13B002C3090 /* Diffing.swift in Sources */,
258E31B31F0D8D9C00D6F324 /* TableViewDriver.swift in Sources */,
32753F8D201BB8310084DCB1 /* UICollectionView+Extensions.swift in Sources */,
Expand Down
136 changes: 136 additions & 0 deletions Sources/CellParentViewProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// 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 Foundation
import UIKit

/**
This protocol unifies `UICollectionView` and `UITableView` by providing a common dequeue method for cells.
It describes a view that is the "parent" view for a cell.
For `UICollectionViewCell`, this would be `UICollectionView`.
For `UITableViewCell`, this would be `UITableView`.
*/
protocol CellParentViewProtocol {

/// The type of cell for this parent view.
associatedtype CellType: UIView

/// The type of supplementary view for this parent view.
associatedtype SupplementaryType: UIView

func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType

func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType?

func registerCellClass(_ cellClass: AnyClass?, identifier: String)
func registerCellNib(_ cellNib: UINib?, identifier: String)

func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String)
func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String)
}

extension CellParentViewProtocol {
func registerCellViewModels(_ cellViewModels: [ReusableCellProtocol]) {
cellViewModels.forEach {
self.registerCellViewModel($0)
}
}

func registerCellViewModel(_ model: ReusableCellProtocol) {
let info = model.registrationInfo
let identifier = info.reuseIdentifier
let method = info.registrationMethod

switch method {
case let .fromClass(classType):
self.registerCellClass(classType, identifier: identifier)
case .fromNib:
self.registerCellNib(method.nib, identifier: identifier)
}
}

func registerSupplementaryViewModel(_ model: ReusableSupplementaryViewProtocol) {
guard let info = model.viewInfo else { return }
let identifier = info.reuseIdentifier
let method = info.registrationMethod
let kind = info.kind

switch method {
case let .fromClass(classType):
self.registerSupplementaryClass(classType, kind: kind, identifier: identifier)
case .fromNib:
self.registerSupplementaryNib(method.nib, kind: kind, identifier: identifier)
}
}
}

extension UICollectionView: CellParentViewProtocol {
typealias CellType = UICollectionViewCell
typealias SupplementaryType = UICollectionReusableView

func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType {
return self.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
}

func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType? {
return self.dequeueReusableSupplementaryView(ofKind: kind.collectionElementKind, withReuseIdentifier: identifier, for: indexPath)
}

func registerCellClass(_ cellClass: AnyClass?, identifier: String) {
self.register(cellClass, forCellWithReuseIdentifier: identifier)
}

func registerCellNib(_ cellNib: UINib?, identifier: String) {
self.register(cellNib, forCellWithReuseIdentifier: identifier)
}

func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String) {
self.register(supplementaryClass, forSupplementaryViewOfKind: kind.collectionElementKind, withReuseIdentifier: identifier)
}

func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String) {
self.register(supplementaryNib, forSupplementaryViewOfKind: kind.collectionElementKind, withReuseIdentifier: identifier)
}
}

extension UITableView: CellParentViewProtocol {
typealias CellType = UITableViewCell
typealias SupplementaryType = UITableViewHeaderFooterView

func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType {
return self.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
}

func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType? {
return self.dequeueReusableHeaderFooterView(withIdentifier: identifier)
}

func registerCellClass(_ cellClass: AnyClass?, identifier: String) {
self.register(cellClass, forCellReuseIdentifier: identifier)
}

func registerCellNib(_ cellNib: UINib?, identifier: String) {
self.register(cellNib, forCellReuseIdentifier: identifier)
}

func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String) {
self.register(supplementaryClass, forHeaderFooterViewReuseIdentifier: identifier)
}

func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String) {
self.register(supplementaryNib, forHeaderFooterViewReuseIdentifier: identifier)
}
}
4 changes: 3 additions & 1 deletion Sources/CollectionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ public extension CollectionViewCellViewModel {
}

/// View model for supplementary views in collection views.
public protocol CollectionViewSupplementaryViewModel {
public protocol CollectionViewSupplementaryViewModel: ReusableSupplementaryViewProtocol {

/// Metadata for this supplementary view.
var viewInfo: SupplementaryViewInfo? { get }

/// Height of this supplementary view.
var height: CGFloat? { get }

Expand Down
6 changes: 6 additions & 0 deletions Sources/SupplementaryViewInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
import Foundation
import UIKit

public protocol ReusableSupplementaryViewProtocol {

/// The registration info for the supplementary view.
var viewInfo: SupplementaryViewInfo? { get } // TODO: make this not optional
}

/// Metadata thats required for setting up a supplementary view.
public struct SupplementaryViewInfo {
/// Stores how the view was registered (as a class or via a nib file)
Expand Down
10 changes: 2 additions & 8 deletions Sources/TableViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ import Dwifft
import UIKit

/// View model for the individual cells of a `TableViewModel`.
public protocol TableViewCellViewModel {
public protocol TableViewCellViewModel: ReusableCellProtocol {

/// `TableViewDriver` will automatically apply an `accessibilityIdentifier` to the cell based on this format.
var accessibilityFormat: CellAccessibilityFormat { get }

/// The registration info for the cell.
var registrationInfo: ViewRegistrationInfo { get }

/// The height of this cell.
var rowHeight: CGFloat { get }

Expand Down Expand Up @@ -83,17 +80,14 @@ public protocol TableViewCellModelEditActions {

/// Protocol that needs to be implemented by custom header
/// footer view models.
public protocol TableViewSectionHeaderFooterViewModel {
public protocol TableViewSectionHeaderFooterViewModel: ReusableSupplementaryViewProtocol {

/// The title of the header
var title: String? { get }

/// The height of the header
var height: CGFloat? { get }

/// Metadata about the custom view type
var viewInfo: SupplementaryViewInfo? { get }

/// Asks the view model to update the header/footer
/// view with the content in the model.
/// - Parameter view: the header/footer view
Expand Down
21 changes: 4 additions & 17 deletions Sources/UICollectionView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,14 @@ extension UICollectionView {

func registerViews(for model: CollectionViewModel) {
model.sectionModels.forEach {
// TODO: collection cells

if let header = $0.headerViewModel {
self._registerSupplementaryViewModel(header)
self.registerSupplementaryViewModel(header)
}

if let footer = $0.footerViewModel {
self._registerSupplementaryViewModel(footer)
}
}
}

private func _registerSupplementaryViewModel(_ viewModel: CollectionViewSupplementaryViewModel) {
if let viewInfo = viewModel.viewInfo {
switch viewInfo.registrationMethod {
case let .fromNib(name, bundle):
self.register(UINib(nibName: name, bundle: bundle),
forSupplementaryViewOfKind: viewInfo.kind.collectionElementKind,
withReuseIdentifier: viewInfo.reuseIdentifier)
case let .fromClass(viewClass):
self.register(viewClass,
forSupplementaryViewOfKind: viewInfo.kind.collectionElementKind,
withReuseIdentifier: viewInfo.reuseIdentifier)
self.registerSupplementaryViewModel(footer)
}
}
}
Expand Down
41 changes: 7 additions & 34 deletions Sources/UITableView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,15 @@ extension UITableView {

func registerViews(for model: TableViewModel) {
model.sectionModels.forEach {
self._registerCells($0.cellViewModels)
self._registerHeaderFooterViewModel($0.headerViewModel)
self._registerHeaderFooterViewModel($0.footerViewModel)
}
}

private func _registerCells(_ cellViewModels: [TableViewCellViewModel]) {
cellViewModels.forEach {
self._registerCellViewModel($0)
}
}

private func _registerCellViewModel(_ viewModel: TableViewCellViewModel) {
let registrationInfo = viewModel.registrationInfo
let identifier = registrationInfo.reuseIdentifier
let method = registrationInfo.registrationMethod

switch method {
case let .fromClass(classType):
self.register(classType, forCellReuseIdentifier: identifier)
case .fromNib:
self.register(method.nib, forCellReuseIdentifier: identifier)
}
}

private func _registerHeaderFooterViewModel(_ viewModel: TableViewSectionHeaderFooterViewModel?) {
guard let viewInfo = viewModel?.viewInfo else { return }
self.registerCellViewModels($0.cellViewModels)

let identifier = viewInfo.reuseIdentifier
let method = viewInfo.registrationMethod
if let header = $0.headerViewModel {
self.registerSupplementaryViewModel(header)
}

switch method {
case .fromNib:
self.register(method.nib, forHeaderFooterViewReuseIdentifier: identifier)
case let .fromClass(classType):
self.register(classType, forHeaderFooterViewReuseIdentifier: identifier)
if let footer = $0.footerViewModel {
self.registerSupplementaryViewModel(footer)
}
}
}
}
6 changes: 6 additions & 0 deletions Sources/ViewRegistrationInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
import Foundation
import UIKit

public protocol ReusableCellProtocol {

/// The registration info for the cell.
var registrationInfo: ViewRegistrationInfo { get }
}

public struct ViewRegistrationInfo {
let reuseIdentifier: String
let registrationMethod: ViewRegistrationMethod
Expand Down

0 comments on commit ad69c94

Please sign in to comment.