Create a custom subclass of UITableViewController for each table view that you manage.

This is a suggestion in the document of UITableViewController. We can indeed ignore this suggestion since it’s more flexible to implement the UITableViewDataSource and the UITableViewDelegate directly, but the UITableViewController does more than set the data source and the delegate.

Behaviors Implemented by UITableViewController

If there is any behavior implemented by UITableViewController that it’s hard for us to implement by ourselves, we should listen to the suggestion.

So let’s check the behaviors one by one.

It automatically loads the table view archived in a storyboard or nib file. Access the table view using the tableView property.

We can achieve this behavior by @IBOutlet.

It sets the data source and the delegate of the table view to self.

Of course, we can set them by ourselves.

It implements the viewDidAppear(_:) method and automatically flashes the table view’s scroll indicators when it first appears.

This is a reasonable way to tell the user that the content is scrollable, but we can also achieve by ourselves with one line code:

tableView.flashScrollIndicators()

It implements the setEditing(_:animated:) method and automatically toggles the edit mode of the table when the user taps an Edit|Done button in the navigation bar.

We do not always need this behavior, and we can achieve it by UINavigationItem if we need it.

It automatically resizes its table view to accommodate the appearance or disappearance of the onscreen keyboard.

We also do not always need this behavior, and we can achieve it by UITableView.contentInset if we need it.

It implements the viewWillAppear(_:) method and automatically reloads the data for its table view on first appearance. It clears its selection (with or without animation, depending on the request) every time the table view is displayed; you can disable this behavior by changing the value in the clearsSelectionOnViewWillAppear property.

The last one, it seems like we can achieve this behavior by:

override func viewWillAppear(_ animated: Bool) {
    ...
    if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
        tableView.deselectRow(at: indexPathForSelectedRow, animated: true)
    }
}

But if we try to use the interactive pop gesture, we will find out that UITableViewController will change the selection state gradually along with the gesture instead of deselecting the row once viewWillAppear called like the implementation above. And this behavior even works on a custom UITableViewCell.selectedBackgroundView.

Sure that we can observe the UINavigationController.interactivePopGestureRecognizer and get the pop progress from UIViewController.transitionCoordinator?.percentComplete to modify UITableViewCell.selectedBackgroundView according to the progress. But this implementation is a little complicate, we had better to use UITableViewController since it’s added from iOS 3.2 which means it’s widely tested.

Finally, here is a reason to listen to that suggestion.

Demerits

However, you may think that there are also some demerits when using UITableViewController. Let’s check if we can overcome them effortlessly.

The Root View is a UITableView

If we use a UITableView, we can add it to the root view and layout it as we want. But when we use UITableViewController, the UITableView is the root view, the only thing we can do is to add other views on the UITableView.

However, we can overcome this by making the UITableViewController as a child view controller than layout it’s view as we want just like we layout a UITableView.

addChild(tableViewController)
view.addSubView(tableViewController.tableView!)
// Set frame or Auto Layout constrants
tableViewController.didMove(toParent: self)

And all behaviors that UITableViewController implemented will still work.

Have to Make a Subclass for Each UITableView

Since the UITableViewController itself is the UITableViewDataSource and the UITableViewDelegate of its UITableView, so we may have to make a subclass of UITableViewController for a different UITableViewDataSource/UITableViewDelegate.

However, we can create a common subclass to overcome this demerit since our goal is to use UITableViewController like a UITableView.

class TableViewController: UITableViewController {
    var dataSource: UITableViewDataSource?
    var delegate: UITableViewDelegate?
    // Override all methods of UITableViewDataSource and UItableViewDelegate
    // to forward to `dataSource` and `delegate`
}

Other Demerits

There may be other demerits that I haven’t noticed, but with the code above, we can use UITableViewController like a UITableView, so there should be some ways to overcome them painlessly.

A Tip for Popover or iOS 13 Style Modal

You have noticed that a popover or iOS 13 style modal breaks the behavior of clearing the selection since dismissing such a modal will not call viewWillAppear(_:).

We can call viewWillAppear(_:) by ourselves to solve this issue. This approach may not be a proper way for a normal UIViewController since it’s a lifecycle method, but if we only use UITableViewController like a UITableView, I think it’s alright.

Set UIAdaptivePresentationControllerDelegate before present a view controller.

viewController.presentationController?.delegate = self
present(viewController, animated: true)

Call the UITableViewController’s viewWillAppear(_:) when the modal is going to dismiss.

extension ViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
        tableViewController.viewWillAppear(true)
    }
}

That’s it. The behavior of clearing the selection will work again.

Conclution

Since we can use UITableViewController like a UITableView effortlessly, let’s listen to that suggestion and get all merits of UITableViewController for free.