9

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):

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?

Community
  • 1
  • 1
Sunkas
  • 9,542
  • 6
  • 62
  • 102

3 Answers3

1

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.

Sunkas
  • 9,542
  • 6
  • 62
  • 102
  • I have exactly the same issue as yours. I want to have a generic data source and various configurable cells types with a parent class. Did you find a solution for this issue ? – rokridi Mar 13 '19 at 19:29
  • Nope. Seems like a Swift limitation. I use this pattern more and more now, but I still need to cast the cellModel: func setup(from cellModel: CellModel) { guard let cellModel = cellModel as? MyCellModel else { return } .... } – Sunkas Mar 14 '19 at 10:15
  • Thank you for the answer. That was the solution that I also implemented. But i consider it as nasty solution since the cell is tightly coupled to MyCellModel and that breaks POP concept :/ – rokridi Mar 14 '19 at 19:03
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
    }
 }
Nate Mann
  • 695
  • 6
  • 17
  • I try to do the same with cellwilldisplay tableview delegate method by only changing type of the cell paramerter. it doesn't work. the method is not considered as an overriding. maybe they didnt implement it this way. – hasan Mar 17 '17 at 15:22
  • Thanks for trying to help me. This could be a attempt in the right direction. However, it complains on my subclasses of DataSource. As you added CellDataSetupable as a generic parameter, I have to input some thing to this, but it produces a `Using a CellDataSetupable as a concrete type conforming to protocol CellDataSetupable is not supported`. See "Edit 1" in question above for the more code. – Sunkas Mar 20 '17 at 08:10
0

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!

timaktimak
  • 1,380
  • 1
  • 12
  • 21
  • Thanks for your answer. However, I cannot add `HomeTableViewCell` as a concrete as the DataSource can have different types. It's quite normal that a `UITableView` have different types of cells and subclasses of `UITableViewCell`s in them. I do not want to limit myself to one of these types. The problem I had was that I want to use a protocol for these cells so that I can check if the are "setupable" and then call the setup method of them. This would be quite handy. It's solvable with a base class for "HomeTableViewCell" and "Blurb...", but then I have to cast the parameters with I do not want – Sunkas Mar 21 '17 at 09:17
  • Yes, I understand. My answer is an addition to what Nate Mann suggested and how to solve the problem that you described in Edit 1. Once again, in the dataSouce declaration you should use a generic parameter `Cell: CellDataSetupable`, but when you use the class to instantiate a concrete object, you use a concrete `UITableViewCell` subclass. Or, if you want a subclass to also work with different types of cells, you can make it generic too. See the edit. – timaktimak Mar 21 '17 at 10:12
  • I mentioned briefly in my question that creating a generic subclass of UIView objects is not supported in Interface Builder. I now added `Edit 2` to highlight this, also with a link to another SO question. I've already tried to make a concrete subclass of a generic UITableViewCell-class without success. – Sunkas Mar 21 '17 at 10:27
  • I did not suggest creating a generic UIView subclass. Check out the code that I have provided, it works and it does what you want. – timaktimak Mar 21 '17 at 10:31
  • It still does not allow you to use different types of UITableViewCells in the same DataSource (or SubDataSource). "TableViewCell" in your example is a class I assume? I want that to be a protocol that many different `UITableViewCell` subclasses can implement. – Sunkas Mar 22 '17 at 14:42