iosswiftuitableviewuikituitableviewdiffabledatasource

UITableViewDelegate doesn't work with UITableViewDiffableDataSource


I created test project to reproduce the issue https://github.com/msnazarow/DiffarableTest

Simply you need to pass several steps

  1. Create new target
  2. Create Base class that inherit UITableViewDiffableDataSource and also UITableViewDelegate but do not implement UITableViewDelegate methods
  3. In another target(main for simplify) inherit Base class and implement any of UITableViewDelegate method (for example didSelectRowAt)
  4. None of implementing methods would work

Solution

  • You have to configure the delegate funcs in your BaseDataSource class to be overridden in subclasses.

    So, first step, comment-out your didSelectRowAt func in extension MyDataSource:

    extension MyDataSource {
    //  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    //      print("CELL SELECTED")
    //  }
    }
    

    and implement didSelectRowAt in BaseDataSource:

    open class BaseDataSource<T: Model & Hashable>: UITableViewDiffableDataSource<Int, T>, UITableViewDelegate {
        public init(tableView: UITableView) {
            super.init(tableView: tableView) { tableView, indexPath, model in
                let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! Configurable
                cell.configure(with: model)
                return cell as? UITableViewCell
            }
        }
        public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("Base Cell Selected", indexPath)
        }
    }
    

    When you run the app and tap on the 3rd row, you should get debug output:

    Base Cell Selected [0, 2]
    

    To implement that in your subclass, you can override it:

    extension MyDataSource {
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("MyDataSource Cell Selected", indexPath)
        }
    }
    

    You'll get an error: Cannot override a non-dynamic class declaration from an extension ... so make it dynamic in BaseDataSource:

    open class BaseDataSource<T: Model & Hashable>: UITableViewDiffableDataSource<Int, T>, UITableViewDelegate {
        public init(tableView: UITableView) {
            super.init(tableView: tableView) { tableView, indexPath, model in
                let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! Configurable
                cell.configure(with: model)
                return cell as? UITableViewCell
            }
        }
        public dynamic func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("Base Cell Selected", indexPath)
        }
    }
    

    Tapping on the 3rd row now should output:

    MyDataSource Cell Selected [0, 2]
    

    Note that didSelectRowAt in BaseDataSource will be called if there is no sub-classed delegate. Plus, if you want some code to also run in didSelectRowAt in BaseDataSource, you can call super from the subclass:

    extension MyDataSource {
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            super.tableView(tableView, didSelectRowAt: indexPath)
            print("MyDataSource Cell Selected", indexPath)
        }
    }
    

    and selecting the 3rd row outputs:

    Base Cell Selected [0, 2]
    MyDataSource Cell Selected [0, 2]
    

    Edit

    After a little more research, this is considered by some to be a "bug" as it seems to have changed between Swift versions.

    However, one big change along the way is that Swift originally did, but no longer, infers @objc. Everyone ran into that when it became necessary to add @objc to funcs such as when used in selectors.

    So, two ways to handle this...

    First, as shown above, implement "do nothing" funcs for any table view delegate funcs you want to override in your subclass.

    The second option, which sounds like you would prefer, is to declare the @objc method inside your subclass (or its extension):

    extension MyDataSource {
        // add this line
        @objc (tableView:didSelectRowAtIndexPath:)
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("MyDataSource extension Cell Selected", indexPath)
        }
    }
    

    Now you can go back to your original code, and you only need to add that one line to get the functionality needed.