|
| 1 | +# ReusableDataSource |
| 2 | + |
| 3 | +Never again write a custom UITableView or UICollectionView data source. **Disclamer**: not really, but this is a good start. |
| 4 | + |
| 5 | +## Implementation |
| 6 | + |
| 7 | +Implement ```ReusablePresenter``` protocol on a reusable view. |
| 8 | + |
| 9 | +```Swift |
| 10 | +class TextTableViewCell: UITableViewCell, ReusablePresenter { |
| 11 | + func present(viewModel: String) { |
| 12 | + textLabel?.text = viewModel |
| 13 | + } |
| 14 | +} |
| 15 | +``` |
| 16 | + |
| 17 | +Create view models and specify presenter types using ```ReusableViewModel``` struct. |
| 18 | + |
| 19 | +```Swift |
| 20 | +let viewModels = [ |
| 21 | + ReusableViewModel<TextTableViewCell>(viewModel: "Cell 1").anyPresentable, |
| 22 | + ReusableViewModel<TextTableViewCell>(viewModel: "Cell 2").anyPresentable, |
| 23 | + ReusableViewModel<ImageTextTableViewCell>( |
| 24 | + viewModel: ImageTextTableViewCellViewModel( |
| 25 | + textViewModel: "Cell 3", |
| 26 | + imageViewModel: #imageLiteral(resourceName: "filter") |
| 27 | + ) |
| 28 | + ).anyPresentable, |
| 29 | + ReusableViewModel<TextTableViewCell>(viewModel: "Cell 2").anyPresentable |
| 30 | +] |
| 31 | +``` |
| 32 | + |
| 33 | +Create a ```ReusableTableViewDataSource``` and present the view models. |
| 34 | + |
| 35 | +```Swift |
| 36 | +let dataSource = ReusableTableViewDataSource() |
| 37 | + |
| 38 | +tableView.dataSource = dataSource |
| 39 | + |
| 40 | +dataSource.present(presentableViewModels: reusableViewModels, on: tableView) |
| 41 | +``` |
| 42 | + |
| 43 | +That's it! The reusable data source manages the cell creation and data presentation. Check it out by running the demo project. |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +## Installation |
| 48 | + |
| 49 | +### Carthage |
| 50 | + |
| 51 | +``` |
| 52 | +github "Rep2/ReusableDataSource" ~> 0.1 |
| 53 | +``` |
| 54 | + |
| 55 | +## Detailed overview |
| 56 | + |
| 57 | +```ReusablePresenter``` defines a view model type that the reusable view can present. |
| 58 | + |
| 59 | +```Swift |
| 60 | +protocol ReusablePresenter { |
| 61 | + associatedtype ViewModel |
| 62 | + |
| 63 | + static var source: ReusablePresenterSource { get } |
| 64 | + |
| 65 | + func present(viewModel: ViewModel) |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +To satisfy this protocol all that is needed is to implement ```present(viewModel: ViewModel)``` function. ```associatedtype``` is inferred from the function. |
| 70 | + |
| 71 | +```source``` value determines how the reusable view is created. It's default value is ```class```. Ignore it for now. |
| 72 | + |
| 73 | +```Swift |
| 74 | +enum ReusablePresenterSource { |
| 75 | + case nib |
| 76 | + case `class` |
| 77 | + case storyboard |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +```ReusableViewModel``` connects the presenter and the view model that the presenter knows how to present. |
| 82 | + |
| 83 | +```Swift |
| 84 | +struct ReusableViewModel<Presenter: ReusablePresenter> { |
| 85 | + let viewModel: Presenter.ViewModel |
| 86 | + |
| 87 | + init(viewModel: Presenter.ViewModel) { |
| 88 | + self.viewModel = viewModel |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Simply create it by specifying ```ReusablePresenter``` type and passing the associated ```ViewModel```. |
| 94 | + |
| 95 | +```Swift |
| 96 | +ReusableViewModel<TextTableViewCell>(viewModel: "Cell 1") |
| 97 | +``` |
| 98 | + |
| 99 | +```ReusableTableViewDataSource``` and ```ReusableCollectionViewDataSource``` do the hard work. Initialize and set them as ```UITableView``` or ```UICollectionView``` data source. |
| 100 | + |
| 101 | +```Swift |
| 102 | +let dataSource = ReusableTableViewDataSource() |
| 103 | + |
| 104 | +tableView.dataSource = dataSource |
| 105 | +``` |
| 106 | + |
| 107 | +To be able to mix and match different view models we need to do something called ```Type erasure```. Take a look at this article to get the gist of it [Swift: Attempting to Understand Type Erasure](https://www.natashatherobot.com/swift-type-erasure/). |
| 108 | + |
| 109 | +Basically, we need to remove the generic part of a ```ReusableViewModel``` to be able to put it into an array. |
| 110 | + |
| 111 | +Generic part removal is done by ```AnyTableViewPresentableViewModel``` and ```AnyCollectionViewPresentableViewModel``` for ```UITableViewCell``` and ```UICollectionViewCell``` respectively. The process is simmilar to how Swift Stdlib handles ```Type erasure```. I got the motivation from [Type-erasure in Stdlib](http://robnapier.net/type-erasure-in-stdlib). |
| 112 | + |
| 113 | +```Swift |
| 114 | +class AnyTableViewPresentableViewModel { |
| 115 | + let dequeueAndPresentCellCallback: (UITableView) -> UITableViewCell |
| 116 | + let registerCellCallback: (UITableView) -> Void |
| 117 | + |
| 118 | + init<Presenter: ReusablePresenter>(base: ReusableViewModel<Presenter>) where Presenter: UITableViewCell { |
| 119 | + self.dequeueAndPresentCellCallback = { (tableView: UITableView) -> UITableViewCell in |
| 120 | + tableView.dequeueAndPresent(presentableViewModel: base, for: IndexPath(item: 0, section: 0)) |
| 121 | + } |
| 122 | + |
| 123 | + self.registerCellCallback = { (tableView: UITableView) in |
| 124 | + tableView.register(cell: Presenter.self, reusableCellSource: Presenter.source) |
| 125 | + } |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +They remove the generic part of a ```ReusableViewModel``` keeping the information we need -> how to register and dequeue the cell. |
| 131 | + |
| 132 | +Property ```anyPresentable``` of ```ReusableViewModel``` can be used to simplify the process. |
| 133 | + |
| 134 | +```Swift |
| 135 | +extension ReusableViewModel where Presenter: UITableViewCell { |
| 136 | + var anyPresentable: AnyTableViewPresentableViewModel { |
| 137 | + return AnyTableViewPresentableViewModel(base: self) |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +extension ReusableViewModel where Presenter: UICollectionViewCell { |
| 142 | + var anyPresentable: AnyCollectionViewPresentableViewModel { |
| 143 | + return AnyCollectionViewPresentableViewModel(base: self) |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +Finally, pass the ```Type erased``` view models to the reusable data source. |
| 149 | + |
| 150 | +```Swift |
| 151 | +dataSource.present(presentableViewModels: viewModels, on: tableView) |
| 152 | +``` |
| 153 | + |
| 154 | +Reusable data sources use the ```PresentableViewModel``` dequeue method to create the cell of a proper type and present the view model. |
| 155 | + |
| 156 | +```Swift |
| 157 | +extension UITableView { |
| 158 | + /** |
| 159 | + Returns a "cell" on which `presentableViewModel` was presented. |
| 160 | +
|
| 161 | + - Important: Causes the app to crashes with `NSInternalInconsistencyException` if the `PresentingCell` type isn't previously registered. |
| 162 | + */ |
| 163 | + func dequeueAndPresent<Presenter: ReusablePresenter>(presentableViewModel: ReusableViewModel<Presenter>, for indexPath: IndexPath) -> Presenter |
| 164 | + where Presenter: UITableViewCell { |
| 165 | + let cell = dequeueReusableCell(for: indexPath) as Presenter |
| 166 | + |
| 167 | + cell.present(viewModel: presentableViewModel.viewModel) |
| 168 | + |
| 169 | + return cell |
| 170 | + } |
| 171 | + |
| 172 | + /** |
| 173 | + Registers a reusable "cell" using `CustomStringConvertible` as the reuese identifier. |
| 174 | +
|
| 175 | + - Important: Call before `dequeueReusableCell(for:)` to avoid `NSInternalInconsistencyException`. |
| 176 | + */ |
| 177 | + public func register<T: UITableViewCell>(cell: T.Type, reusableCellSource: ReusablePresenterSource) { |
| 178 | + switch reusableCellSource { |
| 179 | + case .nib: |
| 180 | + register(UINib(nibName: String(describing: cell), bundle: nil), forCellReuseIdentifier: String(describing: cell)) |
| 181 | + case .class, .storyboard: |
| 182 | + register(T.self, forCellReuseIdentifier: String(describing: cell.self)) |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + Returns a "cell" of a given type using `CustomStringConvertible` as the reuese identifier. |
| 188 | +
|
| 189 | + - Important: Force unwraps the "cell". Causes the app to crashes with `NSInternalInconsistencyException` if the cell type isn't previously registered. |
| 190 | + */ |
| 191 | + public func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T { |
| 192 | + return dequeueReusableCell(for: indexPath)! |
| 193 | + } |
| 194 | + |
| 195 | + /** |
| 196 | + Returns an optional "cell" of a given type using `CustomStringConvertible` as the reuese identifier. |
| 197 | +
|
| 198 | + - Important: Causes the app to crashes with `NSInternalInconsistencyException` if the cell type isn't previously registered. |
| 199 | + */ |
| 200 | + public func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T? { |
| 201 | + return dequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath) as? T |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +This is also where the ```ReusablePresenterSource``` comes into play. Data source automatically registers ```reuseIdentifier``` based on ```ReusablePresenter.source``` property. To disable this behavior set data sources ```automaticallyRegisterReuseIdentifiers``` to ```fasle```. |
0 commit comments