Xcoding with Alfian

Software Development Videos & Tutorials

Building Reusable Generic UITableViewController in iOS App

Alt text

TableView Controller is an essential UIKit component that is almost used in every iOS app out there to present collection of data in list. When we have different type of data that we want to display in the UITableViewController, most of the time we create a new subclass to display the related type of data. This approach works, but can lead to repetition and difficulty in maintenance if we have many different types of data in our application.

How can we approach and solve this problem?. One of the way is we can use simple abstraction using Swift Generic Abstract Data Type to create Generic UITableViewController subclass that can be used to configure and display different kind of data using Swift Generic Constraint.

You can find and build the source project in the GitHub repository at alfianlosari/GenericTableViewController.

Building the Generic TableViewController

We create a subclass of UITableViewController called GenericTableViewController, we add 2 type of Generic T and Cell. We add constraint that Cell must be a UITableViewCell subclass. The T will be used as an abstraction of the data while the Cell will be registered to the UITableView and dequeued to display the data for each row as a UITableViewCell.

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {  var items: [T]
  var configure: (Cell, T) -> Void
  var selectHandler: (T) -> Void  init(items: [T], configure: @escaping (Cell, T) -> Void, selectHandler: @escaping (T) -> Void) {
    self.items = items
    self.configure = configure
    self.selectHandler = selectHandler
    super.init(style: .plain)
    self.tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
  }  ...
}

Let’s take look of the initializer, it accepts 3 arguments:

  1. The array of T Generic: This will be assigned as an instance variable that drives the UITableViewDataSource.
  2. The configuration closure: This configuration closure will be invoked passing the T data and Cell when the tableview dequeue the cell to display in each row. Here, we setup how the UITableViewCell will be displayed using the data. (By declaring the type of the Cell explicitly in the parameter, the compiler will be able to implicitly infer the type of the Cell as long as it is the subclass of a UITableViewCell)
  3. The selected handler closure. This closure will be invoked passing the selected when the row in the cell is selected/tapped by the user. Here, we can add logic or action that will be invoked when user taps on a row.

The initializer assigns each of the 3 arguments as an instance variable of the class, then it registers the Cell to the UITableView with a reusable identifier that can be used to dequeue the UITableViewCell for the data source.

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {  ....  //1
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
  }  
  
  //2  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
    let item = items[indexPath.row]
    configure(cell, item)
    return cell
  }

  //3
  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let item = items[indexPath.row]
    selectHandler(item)
  }
}

Here are the UITableViewDataSource and the UITableViewDelegate methods that we need to override:

  1. tableView:numberOfRowsInSection: : Here we just return the number of data in the array of T object.
  2. tableView:cellForRowAtIndexPath: : We dequeue the UITableViewCell using the reusable identifier and then cast it as Cell. Then, we get the data from the T array using the index path row. After that, we invoke the configuration closure passing the cell and the data for it to be customized before displayed.
  3. tableView:didSelectRowAtIndexPath: : Here we just get our data from the array using the index path row the invoke the selected handler closure passing the data.

Using the GenericTableViewController

To try the GenericTableViewController using different type of object, we create two simple struct, Person and Film. Inside each struct, we create a static computed variable that will return an array of hardcoded stub objects for each struct.

struct Person {
  
  let name: String
  
  static var stubPerson: [Person] {
    return [
      Person(name: "Mark Hamill"),
      Person(name: "Harrison Ford"),
      Person(name: "Carrie Fisher"),
      Person(name: "Hayden Christensen"),
      Person(name: "Ewan McGregor"),
      Person(name: "Natalie Portman"),
      Person(name: "Liam Neeson")
   ]
  }
}struct Film {  let title: String
  let releaseYear: Int  static var stubFilms: [Film] {
    return [
      Film(title: "Star Wars: A New Hope", releaseYear: 1978),
      Film(title: "Star Wars: Empire Strikes Back", releaseYear: 1982),
      Film(title: "Star Wars: Return of the Jedi", releaseYear:  1984),
      Film(title: "Star Wars: The Phantom Menace", releaseYear: 1999),
      Film(title: "Star Wars: Clone Wars", releaseYear: 2003),
      Film(title: "Star Wars: Revenge of the Sith", releaseYear: 2005)]
  }
}

Setting Up the Person GenericTableViewController

let personsVC = GenericTableViewController(items: Person.stubPerson, configure: { (cell: UITableViewCell, person) in
  cell.textLabel?.text = person.name
}) { (person) in
  print(person.name)
}

We will display the list of Person using a standard UITableViewCell Basic style. Here, we instantiate the GenericTableViewController passing the array of Person object. The completion closure uses standard UITableViewCell for the type of Cell, inside the configuration we just assign the textLabel text property using the name of the person. For the selected handler closure, we just print the name of the selected person to the console. You can see the power of Swift implicit type reference here, the compiler will replace the T generic with the Person struct automatically.

Setting Up the Film GenericTableViewController

class SubtitleTableViewCell: UITableViewCell {
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: .subtitle, reuseIdentifier: nil)
  }
  ...
}

For the Film, we will display it using the UITableViewCell with Subtitle style. To be able to do this, we need to create subclass that overrides the default style to use the Subtitle style which we calls SubtitleTableViewCell.

let filmsVC = GenericTableViewController(items: Film.stubFilms, configure: { (cell: SubtitleTableViewCell, film) in
  cell.textLabel?.text = film.title
  cell.detailTextLabel?.text = "\(film.releaseYear)"
}) { (film) in
  print(film.title)
}

We instantiate the GenericTableViewController passing the array of Film object. For the configuration closure, we set the cell type for the Cell parameter explicitly as the SubtitleTableViewCell, then inside the closure we just set the cell textLabel and detailTextLabel text property using the title and release year of the film. For the selected handler closure, we just print the title of the selected film to the console.

Final integration using UITabBarController as Container View Controller

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {  var window: UIWindow?  
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {    

    // Instantiate person and film table view controller
    ...    
    let tabVC = UITabBarController(nibName: nil, bundle: nil)
    tabVC.setViewControllers([
      UINavigationController(rootViewController: personsVC),
      UINavigationController(rootViewController: filmsVC)
    ], animated: false)
    
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = tabVC
    window?.makeKeyAndVisible()
   
    return true
  }
}

To display it in iOS project, we will use a UITabBarController that contains the Person and Film instance of the GenericTableViewController as the ViewControllers. We set the tab bar controller as the UIWindow root view controller and embed each generic table view controller inside a UINavigationController.

Conclusion

We finally managed to crate an abstract container class for the UITableViewController using Swift generic. This approach really helps us to be able to reuse the same UITableViewController with different type of data source that we can still able to customise using the generic Cell that conforms to the UITableViewCell. Swift generic is a really amazing paradigm that we can use to create a very powerful abstraction. Happy Swifting and long live to Crusty 😋.