I want to simplify the calling of a "setup"-method in UITableViewCell subclasses. However, not all setup methods are the same, but their parameters inherit from the same type. Is it possible with generics or protocol to not have to cast the parameter every time?
First I a cellForRow-method like this:
class DataSource<V : UIViewController, T: TableViewCellData, VM: ViewModel> : NSObject, UITableViewDataSource, UITableViewDelegate { var dataCollection: TableViewDataCollection<T>! var viewModel: VM! func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellData = dataCollection.object(for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: cellData.identifier(), for: indexPath) if let setupableCell = cell as? CellDataSetupable { setupableCell.setup(with: cellData, viewModel: viewModel) } return cell } } protocol CellDataSetupable : class { func setup(with cellData: TableViewCellData, viewModel: ViewModel) }
where I setup the cell with cellData and viewModel.
In my (many) custom UITableViewCell subclasses:
extension BlurbTableViewCell : CellDataSetupable { func setup(with cellData: TableViewCellData, viewModel: ViewModel) { guard let cellData = cellData as? HomeViewTableViewCellData else { return } guard let viewModel = viewModel as? HomeViewModel else { return } // Use cellData and viewModel to setup cell appearance } }
where HomeViewTableViewCellData
is subclass of TableViewCellData
and HomeViewModel
is subclass of ViewModel
Instead I want to remove the guards and directly write something like this:
extension BlurbTableViewCell : CellDataSetupable< { func setup(with cellData: HomeViewTableViewCellData, viewModel: HomeViewModel) { // Use cellData and viewModel to setup cell appearance } }
Attempted solutions (that does not work):
However, I cannot use associatedtype/typealias on the protocol as this produces an error on the casting in cellForRow method "Protocol ... can only be used as a generic constraint because it has Self or associated type requirements". I tried this method: "Unknown class in interface builder file" when using class which inherits from a generic class in a Storyboard
I also cannot use a generic baseclass as Interface builder will crash then: How to create generic protocols in Swift? (also see Edit 2 below)
Any ideas or do I have to live with my castings?
Edit 1: After suggestions from Nate Mann below I tried this code (note that I have renamed some generic types):
// This works fine extension TransactionTableViewCell : CellDataSetupable { typealias CellData = HomeViewTableViewCellData typealias VM = HomeViewModel func setup(with cellData: CellData, viewModel: VM) { //Setup cell appearance ... } }
This row also works fine: (note the extra where clause)
class DataSource<VC : UIViewController, TVCD: TableViewCellData, VM: ViewModel, CDS: CellDataSetupable> : NSObject, UITableViewDataSource, UITableViewDelegate where CDS.TVCD == TVCD, CDS.VM == VM { // ... func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellData = dataCollection.object(for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: cellData.identifier(), for: indexPath) if let setupableCell = cell as? CDS { setupableCell.setup(with: cellData, viewModel: viewModel) } return cell }
But changing from
class HomeTableViewDataSource: DataSource<HomeViewController, HomeViewTableViewCellData, HomeViewModel> {
to
class HomeTableViewDataSource: DataSource<HomeViewController, HomeViewTableViewCellData, HomeViewModel, CellDataSetupable> {
give this error:
Using 'CellDataSetupable' as a concrete type confirming to protocol 'CellDataSetupable' is not supported
Edit 2: Using a concrete version of a generic baseclass for a UITableViewCell class is not either possible. See Why can't interface builder use a concrete generic subclass of of UIView?
3 Answers
Answers 1
I think this should work:
class DataSource<V : UIViewController, T: TableViewCellData, VM: ViewModel, X: CellDataSetupable> : NSObject where X.M == VM, X.T == T { var dataCollection: TableViewDataCollection! var viewModel: VM! func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellData = dataCollection.object(for: indexPath) as! T let cell = tableView.dequeueReusableCell(withIdentifier: cellData.identifier(), for: indexPath) if let setupableCell = cell as? X { setupableCell.setup(with: cellData, viewModel: viewModel) } return cell } } protocol CellDataSetupable: class { associatedtype T: TableViewCellData associatedtype M: ViewModel func setup(with cellData: T, viewModel: M) } class BlurbTableViewCell: UITableViewCell, CellDataSetupable { func setup(with cellData: HomeViewTableViewCellData, viewModel: HomeViewModel) { // Use cellData and viewModel to setup cell appearance } }
Answers 2
When you are using DataSource
, you provide concrete types that the DataSource
works with. So,
class HomeTableViewDataSource: DataSource<HomeViewController, HomeViewTableViewCellData, HomeViewModel, CellDataSetupable> {
should really be
class HomeTableViewDataSource: DataSource<HomeViewController, HomeViewTableViewCellData, HomeViewModel, HomeTableViewCell> {
where HomeTableViewCell
is a concrete UITableViewCell
subclass that conforms to CellDataSetupable
(like BlurbTableViewCell
or TransactionTableViewCell
). It will work fine then.
Here is a simple generic TableViewDataSource example that works and perhaps is a little easier to understand.
Generic types:
import UIKit protocol CellDataProtocol { var cellIdentifier: String { get } } protocol ConfigurableCell { associatedtype Data: CellDataProtocol func configure(with data: Data) } class DataSource<CellData: CellDataProtocol, Cell: ConfigurableCell>: NSObject, UITableViewDataSource where Cell.Data == CellData { var data: [CellData]! func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return data.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellData = data[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: cellData.cellIdentifier, for: indexPath) if let cell = cell as? Cell { cell.configure(with: cellData) } return cell } }
Concrete classes that are built on top of the code above:
class DataObject: CellDataProtocol { var cellIdentifier: String { return "Identifier" } // an additional property of the class var number: Int { return 5 } } class TableViewCell: UITableViewCell, ConfigurableCell { func configure(with data: DataObject) { // configure cell UI with data // properties can be used here // since the exact type is known self.textLabel?.text = "\(data.number)" } } let dataSource = DataSource<DataObject, TableViewCell>() class ConcreteDataSource: DataSource<DataObject, TableViewCell> { // additional functionality }
Edit: you subclass can also work with different types of cells if you want.
class SubDataSource<Cell: ConfigurableCell>: DataSource<DataObject, Cell> where Cell.Data == DataObject { // additional functionality } let d = SubDataSource<TableViewCell>()
Hope this helps! Good luck!
Answers 3
My own idea of the answer is that it's not possible at the moment.
Nate Mann's answer requires me to specify a concrete implementation of CellDataSetupable
which I do not want.
timaktimak's answers either needs a concrete implementation as well or requires subclassing of a UIView.
0 comments:
Post a Comment