iOS Tableview with MVC

How to Make it Clear and Enjoy Your Code

Stan Ostrovskiy
iOS App Development
11 min readOct 14, 2016

--

Example Tableview from my recent app

If you build iOS projects, you already know this fact: one of most commonly used component is a UITableView. If you haven’t yet build any project, you still can see a UITableView in many popular iOS app: YouTube, Facebook, Twitter, Medium, most messenger apps, etc. Basically, every time, when you need to display a dynamic number of data objects on the same view, you use UITableView.

Another base component is CollectionView, which I personally prefer to use, because it’s more flexible. I will make another article about CollectionView later.

So, you want to add a UITableView to your project.

The obvious way is to use a UITableViewController, that has a built-in UITableView. It will work with a simple setup, you just need to add your data array and create a cell. It looks easy and works the way we need, except for a few things: the UITableViewController code becomes super-long. And it breaks the MVC Pattern. What is MVC and why we need to even think about it? You can check this article with a great explanation of all iOS Architecture Patterns.

You don’t want to deal with all those patterns? Anyway, you may still want to split your thousand-line-long-UITableViewController.

In my previous post I described Three ways to pass data from Model to Controller.

It this article, I will walk you through the way I deal with tableViews, using the Delegation Method. This approach makes the code look very neat, modular and reusable.

Instead of using one UITableViewController, we will split it to multiple classes:

  • DRHTableViewController: we will make it a subclass of UIViewController, and add a UITableView as a subview
  • DRHTableViewCell: a subclass of UITableViewCell
  • DRHTableViewDataModel: it will make an API call, create data, and return data to the DRHTableViewController using Delegation
  • DRHTableViewDataModelItem: a simple class that will hold all data, that we display in DRHTableViewCell.

Let’s begin with a UITableViewCell.

Part 1: TableViewCell

Start with a new project as a “Single View Application”, and remove default ViewController.swift and Main.storyboard files. We will create all the files we need later, step by step.

First, create a UITableViewCell subclass. If you want to use XIB, check “Also create XIB file”.

For this example, we will reproduce the simplified version of Medium main page. So we will need to add the following subviews:

  1. Avatar Image
  2. Name Label
  3. Date Label
  4. Article Title
  5. Article Preview

Apply the Autolayout the way you want, because the cell design will not affect anything we do in this tutorial. Create an outlet for each subview. In your DRHTableViewCell.swift you will have something similar to this:

class DRHTableViewCell: UITableViewCell {   @IBOutlet weak var avatarImageView: UIImageView?
@IBOutlet weak var authorNameLabel: UILabel?
@IBOutlet weak var postDateLabel: UILabel?
@IBOutlet weak var titleLabel: UILabel?
@IBOutlet weak var previewLabel: UILabel?
}

As you can notice, I changed the default “!” mark to “?” for every @IBOutlet. When you drag your UILabel from the InterfaceBuilder to you code, it will automatically force-unwrap it and add “!” at the end. There are objective-C API compatibility reasons behind this, but I always prefer to avoid force-unwrapping, so I am using optionals instead.

Next, we need to add a method to setup all those labels and image view with data. Instead of using the separate variable for each piece of data, we will create a DRHTableViewDataModelItem:

class DRHTableViewDataModelItem {   var avatarImageURL: String?
var authorName: String?
var date: String?
var title: String?
var previewText: String?
}

It’s better to store a date as Date, but to make it simpler in this example, we will store it as String

All variables are optionals, so we don’t have to worry about the default values. We will provide an Init() later, so for now return to DRHTableViewCell.swift and add the following code, that will configure all the cell labels and imageView with custom data:

func configureWithItem(item: DRHTableViewDataModelItem) {   // setImageWithURL(url: item.avatarImageURL)
authorNameLabel?.text = item.authorName
postDateLabel?.text = item.date
titleLabel?.text = item.title
previewLabel?.text = item.previewText
}

SetImageWithURL method will depend on the way you use image cache in your project, so I will not cover it in this tutorial.

Now, having the cell ready, we can create the TableView.

Part 2: TableView

We will use storyboard-based viewController for this example. You can refer to my previous article to find out the best way to instantiate viewController with storyboard. First, create a UIViewController subclass:

In this project, I use UIViewController instead of UITableViewController, so we have more control. Also, having a UITableView as a subview will allow you to position it the way you need, using Autolayout.

Next, create a storyboard file and name it the same way: DRHTableViewController. Drag a ViewController from the object library and assign it a custom class:

Add a UITableView and pin it to all four view edges:

Finally, add a tableView outlet to DRHTableViewController:

class DRHTableViewController: UIViewController {   @IBOutlet weak var tableView: UITableView?}

We have already created a DRHTableViewDataModelItem class, so add a local variable inside the viewController:

fileprivate var dataArray = [DRHTableViewDataModelItem]()

This variable stores the data, that we will display in tableView.

Note, that we do not create this data inside the ViewController class: the dataArray is an empty array. We will feed it with data later, using the Delegate.

Now, set all the basic tableView properties in viewDidLoad method. You can tweak the colors and styles the way you want. The only property that we need for this example is registerNib:

tableView?.register(nib: UINib?, forCellReuseIdentifier: String)

Instead of creating the nib right before calling this method and hard-typing the cell identifier, we will create both Nib and ReuseIdentifier as class properties in our DRHTableViewCell.

Try to avoid hard-typing any String identifiers in your project. If there is no other way, you can create a string literal and use it instead.

Open DRHTableViewCell and add the following code at the beginning of the class:

class DRHMainTableViewCell: UITableViewCell {   class var identifier: String { 
return String(describing: self)
}
class var nib: UINib {
return UINib(nibName: identifier, bundle: nil)
}
.....}

Save the changes and return to DRHTableViewController. The registerNib method will become as simple as

tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier: DRHTableViewCell.identifier)

Don’t forget to assign tableViewDataSource and TableViewDelegate to self:

override func viewDidLoad() {   super.viewDidLoad()   tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier:   
DRHTableViewCell.identifier)
tableView?.delegate = self
tableView?.dataSource = self
}

Once you do this, compiler will throw you an error: “Cannot assign value of type DRHTableViewController to type UITableViewDelegate”.

When you use UITableViewController subclass, the tableView delegate and datasource are already built-in. If you create a UITableView as a subview of UIViewController, you need to make your UIViewController subclass to inherit from UITableViewControllerDelegate and UITableViewControllerDataSource.

To get rid of the error, just add two extensions to DRHTableViewController class:

extension DRHTableViewController: UITableViewDelegate {}extension DRHTableViewController: UITableViewDataSource {}

You will still see another error: “Type DRHTableViewController does not conform to protocol UITableViewDataSource”. This happens because there are a few required methods, that you need to implement inside this extension:

extension DRHTableViewController: UITableViewDataSource {      func tableView(_ tableView: UITableView, cellForRowAt    
indexPath: IndexPath) -> UITableViewCell {
} func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
}
}

All methods in UITableViewDelegate are optional, so there is no error, if you do not override them. Command-click the UITableViewDelegate to see what methods are available to use. The most common-used are methods to selecting/deselecting the cells, set the cells height, and configure headers/footers of the tableView

As you can see, the above two methods have a return type, so you see the compiler error again: ‘Missing return type”. Lets fix that.

First, we set the number of rows in section: we have already set the dataArray, so we simply use its count:

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

Someone may have noticed, that I didn’t override another method: numberOfSectionsInTableView, that you usually use with UITableViewController. This method is optional and it returns the default value of one. We only have one tableView section in this example, so we don’t need to override this method

The last step in configuring the UITableViewDataSource is to return our custom cell in cellForRowAtIndexPath method:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {   if let cell = tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}
return UITableViewCell()
}

Lets go through this line by line.

To create a cell, we call dequeueReusableCell method with DRHTableViewCell identifier. This returns a UITableViewCell, so we use an optional downcast from UITableViewCell to DRHTableViewCell:

let cell = tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell

Then we safe-unwrap it: if it succeeds, we return the custom cell:

if let cell = tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}

If safe-unwrap fails, we return a default UITableViewCell:

if let cell = tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}
return UITableViewCell()

Did we forget something? Yes, we need to configure our cell with data item:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {   if let cell = tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
cell.configureWithItem(item: dataArray[indexPath.item]) return cell } return UITableViewCell()}

We are ready for the final part: create and connect the DataSource to our TableView.

Part 3: DataModel

Create a DRHDataModel class.

Inside this class we will request the data from either a JSON file, or using HTTP request, or simply from another local dataSource file. This is not a part of this article, so I will assume we already made an API call and it returned an optional array of AnyObject and optional Error:

class DRHTableViewDataModel {   func requestData() {
// code to request data from API or local JSON file will go
here
// this two vars were returned from wherever:
// var response: [AnyObject]?
// var error: Error?
if let error = error {
// handle error
} else if let response = response {
// parse response to [DRHTableViewDataModelItem]
setDataWithResponse(response: response)
}
}
}

In setDataWithResponse method we will build an array of DRHTableViewDataModelItem using response array. Add the following code below the requestData:

private func setDataWithResponse(response: [AnyObject]) {   var data = [DRHTableViewDataModelItem]()   for item in response {
// create DRHTableViewDataModelItem out of AnyObject
}
}

In this method, we create an empty array of DRHTableViewDataModelItem that we need to set with our response. Next, we loop through every item in response array. Inside this loop we need to create DRHTableViewDataModelItem out of AnyObject.

As you remember, we haven’t yet created any Initializer for DRHTableViewDataModel. So return to DRHTableViewDataModel class and add init method. In this case, we are going to use an Optional Init (or Failable Init) with Dictionary [String: String]?.

init?(data: [String: String]?) {if let data = data, let avatar = data[“avatarImageURL”], let name = data[“authorName”], let date = data[“date”], let title = data[“title”], let previewText = data[“previewText”] {self.avatarImageURL = avatar
self.authorName = name
self.date = date
self.title = title
self.previewText = previewText
} else {
return nil
}
}

If any of required paths don’t exist in the Dictionary, or Dictionary itself is a nil, the init will fail (return nil).

With this failable initializer we can complete the setDataWithResponse method in DRHTableViewDataModel class:

private func setDataWithResponse(response: [AnyObject]) {   var data = [DRHTableViewDataModelItem]()   for item in response {     if let drhTableViewDataModelItem =   
DRHTableViewDataModelItem(data: item as? [String: String]) {
data.append(drhTableViewDataModelItem)
}
}
}

At the end of for-loop we will have an array of DRHTableViewDataModelItem ready to use. How are we going to pass this array to the TableView?

Part 4: Delegate

First, create a delegate protocol DRHTableViewDataModelDelegate inside DRHTableViewDataModel.swift file right above the DRHTableViewDataModel class:

protocol DRHTableViewDataModelDelegate: class {}

Inside this protocol, we will create two methods:

protocol DRHTableViewDataModelDelegate: class {   func didRecieveDataUpdate(data: [DRHTableViewDataModelItem])
func didFailDataUpdateWithError(error: Error)
}

The class keyword in the Swift protocol definition limits protocol adoption to class types (and not structures or enums). This is important if we want to use a weak reference to the delegate. We need be sure we do not to create a retain cycle between the delegate and the delegating objects, so we use a weak reference (see below).

Next, add an optional weak property inside the DRHTableViewDataModel class:

weak var delegate: DRHTableViewDataModelDelegate?

Now, we need to add a delegate method, where we want to use it. In this example, we need to pass an Error if the data request fails, and we need to pass data, when we create a data array. Error handler method goes inside the requestData method:

class DRHTableViewDataModel {func requestData() {
// code to request data from API or local JSON file will go
here
// this two vars were returned from wherever:
// var response: [AnyObject]?
// var error: Error?
if let error = error {
delegate?.didFailDataUpdateWithError(error: error)
} else if let response = response {
// parse response to [DRHTableViewDataModelItem]
setDataWithResponse(response: response)
}
}
}

Finally, add second delegate method at the end of setDataWithResponse method:

private func setDataWithResponse(response: [AnyObject]) {   var data = [DRHTableViewDataModelItem]()   for item in response {
if let drhTableViewDataModelItem =
DRHTableViewDataModelItem(data: item as? [String: String]) {
data.append(drhTableViewDataModelItem)
}
}
delegate?.didRecieveDataUpdate(data: data)
}

Now we are ready to pass this data to our tableView.

Part 5: Display Data

With DRHTableViewDataModel we can feed our tableView with data. First, we need to create a reference to this dataModel inside DRHTableViewController:

private let dataSource = DRHTableViewDataModel()

Next, we need to request data. I will do it inside ViewWillAppear, so the data will update every time we open this view:

override func viewWillAppear(_ animated: Bool) {   super.viewWillAppear(true)
dataSource.requestData()
}

This is a simple example, so I request data on viewWillAppear. In the real app, this will depend on multiple factors, such as cache time, API usage, and certain app logic.

Next, assign its delegate to self in ViewDidLoad:

dataSource.delegate = self

Again, you will see a compiler error, because DRHTableViewController does not inherit from DRHTableViewDataModelDelegate yet. Fix it by adding the following code at the end of the file:

extension DRHTableViewController: DRHTableViewDataModelDelegate {   func didFailDataUpdateWithError(error: Error) {   }   func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {   }}

Finally, we need to handle the didFailDataUpdateWithError and didRecieveDataUpdate cases:

extension DRHTableViewController: DRHTableViewDataModelDelegate {   func didFailDataUpdateWithError(error: Error) {
// handle error case appropriately (display alert, log an error, etc.)
} func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {
dataArray = data
}
}

Once we assign data to our local dataArray variable, we are ready to reload the tableView. But instead of doing this in didRecieveDataUpdate method, we will use a property observer to dataArray:

fileprivate var dataArray = [DRHTableViewDataModelItem]() {
didSet {
tableView?.reloadData()
}
}

Setter Property Observer will run the code inside right after the value DidSet, exactly when we need it.

That’s it! Now you have a working prototype of basic tableView setup with custom data source and custom cell. And you don’t have that thousand-line-long-tableViewController class, that holds all the code. Each component you’ve just created can be reused across the whole project, that gives you another advantage.

For your reference, check my github repository with a full source code.

Questions or comments? Don’t hesitate to contact me!

--

--