Subclassing with Nested Generics

Hello. I've got the following classes I intend to use a base:

protocol ListCellViewModel {}

class BaseListCell<ViewModel: ListCellViewModel>: UITableViewCell {
    
    func configure(with model: ViewModel) {
        
    }
    
}

protocol ListViewModel {
    associatedtype CellViewModel: ListCellViewModel
    
    var elements: [CellViewModel] { get }
}

class BaseListViewController<Cell: BaseListCell<CellViewModel>,
                             CellViewModel: ListCellViewModel,
                             ViewModel: ListViewModel>: UITableViewController {
    
    private var viewModel: ViewModel!
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
        cell.configure(with: viewModel.elements[indexPath.row] as! CellViewModel)
        return cell
    }
    
}

So, each cell has a view model, the same goes for the controllers.
Now I can subclass those:

final class ListCell: BaseListCell<ElementViewModel>

struct SomeViewModel: ListViewModel {}

final class ListViewController: BaseListViewController<ListCell<ElementViewModel>, ElementViewModel, SomeViewModel>

As you can see, there's a repetition of ElementViewModel, which is used to fulfil the requirement of BaseListCell. Is there a way to make Swift somehow infer the view model type of ListCell when subclassing BaseListViewController? I'm aiming for the following syntax:

struct SomeViewModel: ListViewModel {}

final class ListViewController: BaseListViewController<ListCell, SomeViewModel>

I'm in no way proficient in the language, so if there's something fundamentally incorrect that I'm doing, I'm open for suggestions.

It seems like you're asking for higher-kinded types. You want to pass the generic type constructor ListCell as the argument for Cell to BaseListViewController, and in the body of BaseListViewController, have Cell be an "abstracted" generic type which can be applied to CellViewModel, like Cell<CellViewModel>.

Swift doesn't support this so you can't get exactly what you want, but you might want to consider using a protocol instead of a BaseListCell class. Perhaps something like:

protocol CellProtocol {
  associatedtype ViewModel : ListCellViewModel
  func configure(with model: ViewModel)
}

class BaseListViewController<Cell: UITableViewCell & CellProtocol,
                             ViewModel: ListViewModel>: UITableViewController {
    ...
    // you can talk about Cell.ViewModel here, which conforms to ListCellViewModel.
}

Now you can write BaseListViewController<ListCell<ElementViewModel>, SomeViewModel>, omitting the second generic parameter.

Edit: You can even put the UITableViewCell superclass requirement on the protocol itself, like

protocol CellProtocol : UITableViewCell {...}

Haven't thought of a protocol, even though I knew that BaseListCell wouldn't have any predefined behaviour. This is good. Thanks.

1 Like

I think I should've created another thread for this one, but still.

Consider the following code:

protocol SomeViewModel { 
  ...
}

class SomeViewController: BaseListViewController<SomeCell, SomeViewModel> { ... }

This isn't going to work since it's required to specify a concrete type as a generic argument. There usually will be a stub (mock) view model implementation and a production one. Can you think of an approach I could take to solve this? Besides type erasure and making the view model protocol a class. The latter would require some code to be written (because we'd need to initialise every property, or making it an optional) for every subclass. To be honest, these 2 ways make the whole idea of a generic list controller not worth it.

Edit: whatever I've just asked was weird, sorry. So far come up with this, seems to work.

protocol FeedViewModel: ListViewModel {
    ...
}

final class StubFeedViewModel: FeedViewModel {
    ...
}

final class FeedViewController<ViewModel: FeedViewModel>: BaseListViewController<FeedCell, ViewModel> {
    
    static func instantiate(with viewModel: ViewModel) -> FeedViewController<ViewModel> {
        let viewController = FeedViewController<ViewModel>()
        viewController.viewModel = viewModel
        return viewController
    }
    
}

let viewController = FeedViewController.instantiate(with: StubFeedViewModel())