Building Reusable Generic UITableViewController in iOS App

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:
An example of Generic UITableViewController implementation - alfianlosari/GenericTableViewControllergithub.com
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:
- The array of
T
Generic: This will be assigned as an instance variable that drives theUITableViewDataSource
. - The configuration closure: This configuration closure will be invoked passing the
T
data andCell
when the tableview dequeue the cell to display in each row. Here, we setup how theUITableViewCell
will be displayed using the data. (By declaring the type of theCell
explicitly in the parameter, the compiler will be able to implicitly infer the type of theCell
as long as it is the subclass of aUITableViewCell
) - 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:
tableView:numberOfRowsInSection:
: Here we just return the number of data in the array ofT
objecttableView:cellForRowAtIndexPath:
: We dequeue theUITableViewCell
using the reusable identifier and then cast it asCell
. Then, we get the data from theT
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.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 😋.