Dispatch issue with generic subclass of custom table view controller

My application has a common base class for all table controllers, and I'm experiencing a strange bug when I define a generic subclass of that table controller base class. The method numberOfSections(in:) never gets called if and only if my subclass is generic.

Below is the smallest reproduction I could make:

class BaseTableViewController: UIViewController {
  let tableView: UITableView

  init(style: UITableViewStyle) {
    self.tableView = UITableView(frame: .zero, style: style)

    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: - Overridden methods

  override func viewDidLoad() {
    super. viewDidLoad()

    self.tableView.frame = self.view.bounds
    self.tableView.delegate = self
    self.tableView.dataSource = self

    self.view.addSubview(self.tableView)
  }
}

extension BaseTableViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    return UITableViewCell(style: .default, reuseIdentifier: nil)
  }
}

extension BaseTableViewController: UITableViewDelegate {
}

Here's the very simple generic subclass:

class ViewController<X>: BaseTableViewController {
  let data: X

  init(data: X) {
    self.data = data
    super.init(style: .grouped)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  func numberOfSections(in tableView: UITableView) -> Int {
    // THIS IS NEVER CALLED!
    print("called numberOfSections")
    return 1
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    print("called numberOfRows for section \(section)")
    return 2
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    print("cellFor: (\(indexPath.section), \(indexPath.row))")
    let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
    cell.textLabel!.text = "foo \(indexPath.row) \(String(describing: self.data))"

    return cell
  }

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print("didSelect: (\(indexPath.section), \(indexPath.row))")
    self.tableView.deselectRow(at: indexPath, animated: true)
  }
}

If I create a simple app that does nothing but display ViewController:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    self.window = UIWindow(frame: UIScreen.main.bounds)

    let nav = UINavigationController(rootViewController: ViewController(data: 3))
    self.window?.rootViewController = nav
    self.window?.makeKeyAndVisible()
    return true
  }
}

The table draws correctly but numberOfSections(in:) is never called! As a result, the table only shows one section (presumably because, according to the docs, UITableView uses 1 for this value if the method isn't implemented).

However, if I remove the generic declaration from the class:

class ViewController: CustomTableViewController {
  let data: Int

  init(data: Int) {
  ....
  }

  // ...
}

then numberOfSections DOES get called!

This behavior doesn't make any sense to me. I can work around it by defining numberOfSections in CustomTableViewController and then having ViewController explicitly override that function, but that doesn't seem like the correct solution: I would have to do it for any method in UITableViewDataSource that has this problem.

1 answer

  • answered 2018-01-11 21:02 Henning Schmid

    Replace your init with coder method:

    required init?(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)
    }
    

    Actually if you have your cell created in Storyboard - I believe that it should be attached to tableView on which you try to create it. And you can remove both of your init methods if you do not perform any logic there.

    Greetings from Germany