SHMTableView vs. UITableViews

A side by side comparison

In the previous article we introduced the SHMTableView library. What we’d now like to do is make a practical comparison between two different approaches to implementing tables with multiple cell types.

The new design of the Showmax app detail screen was aimed to ease browsing through multiple seasons and episodes. We want to eliminate unnecessary steps, so that the user can commence watching as quickly as possible.
An important part of redesign was making episodes as a list. Tapping on an episode would show the new video preview below. For our example, we’ll be making a super simple list of episodes with video previews:

List of episodes with video preview

Preparing common stuff

For both cases, the first step is to create the following common objects. You can explore them more
closely in this example code on GitHub.

  • view models
    • EpisodeCellViewModel
    • VideoCellViewModel
  • view cells conforming to the SHMTableRowProtocol
    • EpisodeTableViewCell + NIB file EpisodeTableViewCell UI
    • VideoTableViewCell + NIB file VideoTableViewCell UI

Implementing the screen

UITableView

class ComparisonUITableViewController: UIViewController
{
    @IBOutlet open weak var tableView: UITableView!

    var items: [Any] = []

    override func viewDidLoad()
    {
        items = [
            EpisodeCellViewModel(number: 7, title: "The One With The Race Car Bed"),
            EpisodeCellViewModel(number: 8, title: "The One With The Giant Poking Device"),
            EpisodeCellViewModel(number: 9, title: "The One With The Football"),
            VideoCellViewModel(title: "Video preview for 'The One With The Football'"),
            EpisodeCellViewModel(number: 10, title: "The One Where Rachel Quits"),
            EpisodeCellViewModel(number: 11, title: "The One Where Chandler Can't Remember Which Sister"),
        ]

        tableView.delegate = self
        tableView.dataSource = self
        tableView.estimatedRowHeight = 44

        tableView?.register(UINib(nibName: "EpisodeTableViewCell", bundle: nil), forCellReuseIdentifier: "EpisodeTableViewCell")
        tableView?.register(UINib(nibName: "VideoTableViewCell", bundle: nil), forCellReuseIdentifier: "VideoTableViewCell")
    }
}

extension ComparisonUITableViewController: UITableViewDataSource
{
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        return items.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        guard indexPath.row < items.count else
        {
            fatalError("Requesting cell on indexPath \(indexPath) out of bounds.")
        }

        let item = items[indexPath.row]

        if  let episode = item as? EpisodeCellViewModel,
            let cell = tableView.dequeueReusableCell(withIdentifier: "EpisodeTableViewCell", for: indexPath) as? EpisodeTableViewCell
        {
            cell.configure(episode)
            return cell

        } else if   let video = item as? VideoCellViewModel,
                    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoTableViewCell", for: indexPath) as? VideoTableViewCell
        {
            cell.configure(video)
            return cell
        }

        fatalError("Cannot create cell for requested indexPath \(indexPath).")
    }
}

extension ComparisonUITableViewController: UITableViewDelegate
{
    public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
    {
        guard indexPath.row < items.count else
        {
            return
        }

        let item = items[indexPath.row]

        if  let episode = item as? EpisodeCellViewModel,
            let cell = tableView.dequeueReusableCell(withIdentifier: "EpisodeTableViewCell", for: indexPath) as? EpisodeTableViewCell
        {
            cell.configureAtWillDisplay(episode)

        } else if   let video = item as? VideoCellViewModel,
                    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoTableViewCell", for: indexPath) as? VideoTableViewCell
        {
            cell.configureAtWillDisplay(video)
        }
    }

    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
    {
        guard indexPath.row < items.count else
        {
            return
        }

        let item = items[indexPath.row]

        if let episode = item as? EpisodeCellViewModel
        {
            episode.openDetail()

        } else if let video = item as? VideoCellViewModel
        {
            video.play()
        }
    }
}

SHMTableView

class ComparisonSHMTableViewController: UIViewController
{
    public var shmTable: SHMTableView!
    @IBOutlet open weak var tableView: UITableView!

    var items: [Any] = []

    override func viewDidLoad()
    {
        items = [
            EpisodeCellViewModel(number: 7, title: "The One With The Race Car Bed"),
            EpisodeCellViewModel(number: 8, title: "The One With The Giant Poking Device"),
            EpisodeCellViewModel(number: 9, title: "The One With The Football"),
            VideoCellViewModel(title: "Video preview for 'The One With The Football'"),
            EpisodeCellViewModel(number: 10, title: "The One Where Rachel Quits"),
            EpisodeCellViewModel(number: 11, title: "The One Where Chandler Can't Remember Which Sister"),
        ]

        let rows = items.flatMap({ item -> SHMTableRowProtocol? in

            if let episode = item as? EpisodeCellViewModel
            {
                return SHMTableRow<EpisodeTableViewCell>(model: episode, action: { _ in episode.openDetail() })

            } else if let video = item as? VideoCellViewModel
            {
                return SHMTableRow<VideoTableViewCell>(model: video, action: { _ in video.play() })

            } else
            {
                return nil
            }
        })

        shmTable = SHMTableView(tableView: tableView)
        shmTable += SHMTableSection(rows: rows)
    }
}


Using plain UITableViewDataSource and UITableViewDelegate

The biggest challenge here is that in each delegate or datasource method you often need to check
the type of displayed cell in order to perform some specific logic with it. When there are many cell types
this means a lot of type checking. In some instances, the code might multiply with each additional delegate/datasource method.
And things can get even more complex when you need perform batch table updates (add/remove/reload).

Using SHMTableView

Basically, the process is to map your existing cell models to cell view types and pass them to the SHMTableView.
This will then take care of cell registration, handle table data sources to produce the correct cell view
and fill this with the corresponding model. In this manner, SHMTableView can be used to avoid repetitive busywork.

Comparison

UITableView SHMTableView
Pros
  • Readable for 1-2 cell types
  • Direct access to delegate and data sources
  • All implementations in one place
  • Readable for 2 or more cell types
  • Controller is uncluttered with common and repetitive UITableView routines
  • Focuses on the model structure to be displayed in the table
  • More flexibility in adding and removing cells
Cons
  • Leads to massive view controllers
    for 3 or more cell types
  • Code duplication within view controller
    • checking types of cells, sections, section headers, section footers and models
  • Code duplication within other similar table-centric screens, especially:
    • animating changed rows
    • setting up self-sizing
  • Requires mapping of cell model to cell view
  • Non-direct access to implemented UITableView protocols (but we plan to provide a way to extend our implementations with specific code, so your code gets called)
Results While it’s possible to develop the same screen with UITableView, when compared to SHMTableView it takes longer and requires more steps. There’s also a risk of forgetting some routine steps. Code duplication might result in the need to make changes in multiples places when refactoring. Using SHMTableView we can implement the screen faster and with considerably less code repetition. This also allows us to create a separate model and view for each cell type that could be reused on other screens as well.

Get involved

You can play with the code yourself. The code used in this article is available at GitHub. You can also download SHMTableView from our official repository.

Come and help us improve the SHMTableView library, contact us at geeks@showmax.com.

Please check the original version of this article at