0

I have a very similar problem to this question and this question and I think they might be related. The difference is that the delegate is not called when a generic view model is used.

In the first example below everything works as expected. Initialize a ViewController, which is a subclass of SimpleListViewController. When the cell is selected it prints "Here" because the superclass (SimpleListViewController) conforms to UICollectionViewDelegate.

class SimpleListVCModel {
    var dataSource: UICollectionViewDiffableDataSource<Section, String>! = nil
    var items = Array(0..<10).map{"This is item \($0)"}

    enum Section: String {
        case main
    }
    
    func configureDataSource(using collectionView: UICollectionView) {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { (cell, indexPath, item) in
            var content = cell.defaultContentConfiguration()
            content.text = "\(item)"
            cell.contentConfiguration = content
        }
        
        dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: String) -> UICollectionViewCell? in
            
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
        }
    }
    
    func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}
class ViewController: SimpleListViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "List"
        view.backgroundColor = .red
        configureHierarchy()
        model.configureDataSource(using: collectionView)
        model.applySnapshot()
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Here")
    }
}
class SimpleListViewController: UIViewController, UICollectionViewDelegate {
    var collectionView: UICollectionView! = nil
    let model = SimpleListVCModel()
    
    func configureHierarchy() {
        let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.delegate = self
        view.addSubview(collectionView)
    }
}

Now If I introduce generics, the collectionView delegate function is never called.

First create a subclass of SimpleListVCModel specifically for the ViewController.

class ViewControllerViewModel: SimpleListVCModel {
    func foo(){
        print("Foo")
    }
}

Then slightly modify SimpleListViewController to use a generic SimpleListVCModel.

class SimpleListViewController<M: SimpleListVCModel>: UIViewController, UICollectionViewDelegate {
    let model: M
    var collectionView: UICollectionView! = nil

    
    //MARK: - Initializer
    init(model: M) {
        self.model = model
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    //MARK: - Views
    func configureHierarchy() {
        let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.delegate = self
        view.addSubview(collectionView)
    }
}

Finally modify ViewController to use a generic version of the SimpleListVCModel.

class ViewController: SimpleListViewController<ViewControllerViewModel> {
    override init(model: ViewControllerViewModel = ViewControllerViewModel()) {
        super.init(model: model)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "List"
        view.backgroundColor = .red
        configureHierarchy()
        model.configureDataSource(using: collectionView)
        model.applySnapshot()
        model.foo()
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Here")
    }
}
Richard Witherspoon
  • 4,082
  • 3
  • 17
  • 33

1 Answers1

0

This does appear to be similar to the case in the answer I linked to in comments, but since it's a little different I'll post this answer here.

First, in your SimpleListViewController, implement didSelectItemAt:

class SimpleListViewController<M: SimpleListVCModel>: UIViewController, UICollectionViewDelegate {
    let model: M
    var collectionView: UICollectionView! = nil
    
    
    //MARK: - Initializer
    init(model: M) {
        self.model = model
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    //MARK: - Views
    func configureHierarchy() {
        let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.delegate = self
        view.addSubview(collectionView)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Selected item: \(indexPath) in SimpleListViewController")
    }

}

When you do that, you'll see an error in ViewController:

Overriding declaration requires an 'override' keyword

For the moment, comment out your didSelectItemAt func in ViewController.

Now, when you select an item, didSelectItemAt in SimpleListViewController will be called and it will print to the debug console.

Next, un-comment didSelectItemAt in ViewController and add the override keyword:

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    print("Selected item: \(indexPath) in ViewController")
}

Running the app now and selecting an item will call that func and print to the debug console.

Note that you can also call super ... so, if you have some default code you want executed in SimpleListViewController:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    print("Selected item: \(indexPath) in SimpleListViewController")
    // do something common
}

and in ViewController:

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    super.collectionView(collectionView, didSelectItemAt: indexPath)
    print("Selected item: \(indexPath) in ViewController")
}

and you'll get both print statements in the debug console.

If you'd rather not do it that way, you can remove the implementation from SimpleListViewController and instead declare the @objc method signature in ViewController:

class ViewController: SimpleListViewController<ViewControllerViewModel> {
    override init(model: ViewControllerViewModel = ViewControllerViewModel()) {
        super.init(model: model)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "List"
        view.backgroundColor = .red
        configureHierarchy()
        model.configureDataSource(using: collectionView)
        model.applySnapshot()
        model.foo()
    }
    
    @objc (collectionView:didSelectItemAtIndexPath:)

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Selected item: \(indexPath) in ViewController")
    }

}

Note that you can no longer call super with that approach.

DonMag
  • 69,424
  • 5
  • 50
  • 86