The Cell Presenter

Introduction

How many times have you written an iOS app, containing a UITableView or UICollectionView, where you end up putting all the cell configuration and updating code in the cell class itself; all the outlets and actions from components in the cell are connected to the cell whereas, with a UIView, they would be connected to a view controller.

So began the quest for a mechanism that would allow us to create a presenter (or controller) that would be responsible for automatically managing the visual state of a cell in response to changes in its model, as well as responding to interaction with the cell’s components by updating the cell’s model.

The Example Model

As is customary with so much example code, we shall use a Person class as the Model to be represented in a custom cell :

class Person : NotifyPropertyChanged
{
  var name: String
  {
    didSet
    {
      onPropertyChanged(\Person.name)
    }
  }
  
  var age: Int
  {
    didSet
    {
      onPropertyChanged(\Person.age)
    }
  }
  
  init(name: String, age: Int)
  {
    self.name = name
    
    self.age = age
  }
  
  lazy var propertyChanged: PropertyChangedEvent<Person> = .init(sender: self)
}


extension Person : Equatable
{
  static func == (lhs: Person, rhs: Person) -> Bool
  {
    return lhs === rhs
  }
}

As you can see, the Person class also implements the NotifyPropertyChanged protocol we discussed in an earlier article. This allows us to notify the cell presenter when changes to the model are made, whether that be from within the cell, or from elsewhere.

Cell Requirements

One problem with Interface Builder is that cell reuse identifiers are entered as strings and the temptation is to use those strings in other code. One thing we can do is to standardise the reuse identifier on the cell class’s type and this can be simply achieved by creating a protocol that encapsulates that behaviour :

public protocol IdentifiableCell { }

extension IdentifiableCell
{
  public static var reuseIdentifier: String
  {
    return String(describing: self)
  }
}

This protocol can be adopted by our custom cell, automatically giving the cell its unique identifier. all that’s needed is to add IdentifiableCell to the cell type’s declaration.

Whilst we are doing that, we might as well specify the designed height of the cell, for later use in the class that implements the UITableViewDelegate protocol :

public class PersonCell : UITableViewCell, IdentifiableCell
{
  public static let rowHeight: CGFloat = 100
  
  @IBOutlet weak var nameLabel: UILabel!

  @IBOutlet weak var ageLabel: UILabel!

  var buttonTapClosure: (() -> ())?

  @IBAction func btnTap()
  {
    buttonTapClosure?()
  }
}

This particular cell contains two labels to display the name and age of the Person, as well as a button that we will use to change the age of the corresponding Person (some might say cruelly) to a random number between 20 and 100.

Of course, there are much more useful things we could do, but this will serve as a simple exercise.

The Base Presenter Class

Following the principles of the Model View Presenter design pattern, we will now need a base presenter class to link the view (cell) to its model :

open class Presenter<modelT, viewT : UIView> : NSObject
{
  public var model: modelT
  {
    didSet
    {
      updateView()
    }
  }
  
  public let view: viewT
  
  public required init(with view: viewT, for model: modelT)
  {
    self.view = view
    
    self.model = model
  }
  
  open func updateView() { }
}

This is then further extended to specialise it to our particular model and cell types :

class PersonCellPresenter : Presenter<Person, PersonCell>, PropertyChangeHandler
{
  public override func updateView()
  {
    // connect model and view to this presenter
    model.propertyChanged += propertyChangeClosure
    
    view.buttonTapClosure =
    {
      self.model.age = Int.random(in: 20...100)
    }
    
    // initialise view to reflect the model's current state
    DispatchQueue.main.async
    {
      self.view.nameLabel.text = self.model.name
      
      self.view.ageLabel.text = String(self.model.age)
    }
  }
  
  lazy var propertyChangeClosure: PropertyChangedEvent.Closure = .init
  {
    // for reference :
    //
    // PropertyChangedEvent : Event<Any, PropertyChangedEvent.Args>
    // {
    //   public struct Args : EventArgs
    //   {
    //     public let keyPath: AnyKeyPath
    //   }
    // }
     
    sender, args in // (sender: Any, args: PropertyChangedEvent.Args)
    
    guard let person = sender as? Person else
    {
      return
    }
    
    DispatchQueue.main.async
    {
      switch args.keyPath
      {
        case \Person.name:
          self.view.nameLabel.text = person.name
        case \Person.age:
          self.view.ageLabel.text = String(person.age)
        default:
          break
      }
    }
  }
}

You may have noticed that the PersonCell class responds to the button tap by calling an optional closure held in the var :

var buttonTapClosure: (() -> ())?

The presenter assigns its own closure to that var, thus enabling it to respond to the button tap here rather than in the cell.

Removing the Data Source Code from the UITableViewController

Usually, the data handling code tends to get put in the UITableViewController subclass, simply because the UITableViewDataSource delegate is implemented there by default. So, because Interface Builder doesn’t allow us to use generic view controller classes, we end up with boilerplate code like this in every table view controller we need to specialise to a given model.

  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  {
    return model.count
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
  {
    let cell = tableView.dequeueReusableCell(withIdentifier: PersonCell.reuseIdentifier, for: indexPath) as! PersonCell
    
    let person = model[indexPath.row]
    
    cell.nameLabel.text = person.name
    
    cell.ageLabel.text = String(person.age)
    
    return cell
  }

This kind of boilerplate code is one of the major causes of Massive View Controller syndrome and it can be so easily avoided by separating the data handling code into a “sub-controller” (in this case)

So, let’s start with a generic class (that I will call an Interactor, in line with the nomenclature used in the Model View Presenter design pattern) to represent a “sub-controller” that handles one specific part of the overall presenter’s work, thus allowing us to write “once only” generic code that can be used in our non-generic UIViewController derived class :

open class TableViewDataSourceInteractor<modelT : Equatable,
viewT : UITableViewCell & IdentifiableCell,
cellPresenterT : CellPresenter<modelT, viewT>> : NSObject, UITableViewDataSource
{
  public var model: [modelT]
  
  public lazy var cellPresenterCache: CellPresenterCache<modelT, viewT, cellPresenterT> = .init()
  
  public init(model: [modelT])
  {
    self.model = model
  }
  
  public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  {
    return model.count
  }
  
  public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
  {
    let cell = tableView.dequeueReusableCell(withIdentifier: viewT.reuseIdentifier, for: indexPath) as! viewT

    let item: modelT = model[indexPath.row]

    self.cellPresenterCache.bind(view: cell, to: item)

    return cell
  }
}

To make life easier, we can specify a typealias adapted to our specific purpose for our table view controller :

typealias PersonTableViewDataSourceInteractor = TableViewDataSourceInteractor<Person, PersonCell, PersonCellPresenter>

All that’s needed now is to make the table view controller aware of this helper interactor :

class ViewController: UITableViewController
{
  private lazy var model: [Person] =
  {
    var items: [Person] = .init()
    
    items.append(Person(name: "Steve Jobs", age: 21))
    
    items.append(Person(name: "Tim Cook", age: 23))
    
    return items
  }()

  private lazy var dataSourceInteractor: PersonTableViewDataSourceInteractor = .init(model: self.model)

  override func viewDidLoad()
  {
    super.viewDidLoad()
    
    tableView.dataSource = dataSourceInteractor
  }
  
  …
}

You may have noticed a couple of innocuous lines, hiding in the code for the generic TableViewDataSourceInteractor class :

  …
  
  public lazy var cellPresenterCache: CellPresenterCache<modelT, viewT, cellPresenterT> = .init()
  
  …
  
    self.cellPresenterCache.bind(view: cell, to: item)
    
  …

And here lies the solution to a problem that had puzzled me for some time.

If, for example, our table view is capable of showing ten cells at a time, then the table view manages a cache of around twelve cells, that will be queued once they have dropped out of the table view; hence the reuse identifier to allow us to recover the cached cells when we move beyond the index paths that were used for the initial display.

Now, it’s all very well and good for the table view to take care of cell recycling but, if we want to create a presenter for each cell that’s likely to be created or dequeued, then we could end up creating one presenter for every item in the model, rather than just for each of the recycled twelve cells

Thus the idea of a cache of presenters that can be “dequeued” in the same way as the cells.

Once again, a typealias helps to make the generic code simpler :

public typealias CellPresenter<itemT, cellT : UITableViewCell> = Presenter<itemT, cellT>
public class CellPresenterCache<itemT, viewT : UITableViewCell, cellPresenterT : CellPresenter<itemT, viewT>>
{
  lazy var presenters: [viewT : cellPresenterT] = .init()
  
  public init() { }
  
  public func bind(view: viewT, to item: itemT)
  {
    guard let presenter = presenters[view] else
    {
      let presenter = cellPresenterT(with: view, for: item)
      
      presenters[view] = presenter
      
      return
    }
    
    presenter.model = item
  }
}

What is happening here is that we are maintaining a dictionary of presenters, keyed on cells.

Every time the table view retrieves a dequeued cell, it is passed to this cache, where we check whether there is already an “active” presenter for that cell in the dictionary. If not, a new presenter is created, bound to its appropriate model and cell, then added to the dictionary; otherwise an existing presenter will be retrieved and its model will be reassigned the item at the required index of the model.

So, there we have it, the CellPresenter, a mechanism for allowing us to manage interactions between items in a list and the cell that represent them, without having to put the code in either the table view controller or the cells; and the CellPresenterCache to manage the recycling of presenters, in line with the table view’s recycling of cells.

Once the generic code is written and placed in a framework, the only “custom” code required for each specific table view, is for the model, the view (cell) and the presenter.

N.B.

I have optimised the NotifyPropertyChanged mechanism, used in this article; I plan on writing a further article on that later.

Summary

This article has discussed how to separate out the UITableViewDataSource delegate from a UITableViewController, thus reducing the code bloat often associated with many view controllers. It also allows us to place a UITableView on a UIViewController should we need to place a table view only on part of the screen.

It also discussed the creation of a cache of cell presenters that can be recycled along with the cell recycling managed by the table view.

Written on June 2, 2019