Outsource your UITableViewDataSource!

UITableViewDataSource

It is very common that the controller of an UITableView is also its data source. This is the number one reason why view controllers tend to become very massive. Outsourcing the data source to an object on its own is a better solution.

Hint: This post has been updated to Swift 3 and Xcode 8.

Let’s take a look at an example:

import UIKit

class TableViewController: UITableViewController {
    
    private var movies = [String]()
    
    //MARK: - UIViewController
    override func viewDidLoad() {
        movies = ["Terminator","Back To The Future","The Dark Knight"]
        
        tableView.reloadData()
    }
    
    //MARK: - UITableViewDataSource

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movies.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")!
        
        cell.textLabel?.text = movies[indexPath.row]
        
        return cell
    }
    
}

The table view displays three cells containing movie titles.  Nevertheless you can see in this simple example that there is already a lot of code in the view controller. If you add all the other stuff a view controller needs to do, it will become massive quickly.

So there is a better solution. We create a class called DataSource and move all the data source methods to that class:

import UIKit

class DataSource: NSObject, UITableViewDataSource {
    
    var movies = [String]()
    
    //MARK: - UITableViewDataSource
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movies.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")!
        
        cell.textLabel?.text = movies[indexPath.row]
        
        return cell
    }
    
}

Now, TableViewController just needs to create an instance of this object and set it as the table view’s data source:

import UIKit

class TableViewController: UITableViewController {
    
    private let dataSource = DataSource()
    
    //MARK: - UITableViewDataSource
    override func viewDidLoad() {
       
        dataSource.movies = ["Terminator","Back To The Future","The Dark Knight"]
        tableView.dataSource = dataSource

    }
}

The view controller is cleaned up now!

More generally, this method works not only with table views but with all views that need a data source, like UICollectionView  or a view you have written on your own.

Video

Resources:

Title Image: @ enzozo / shutterstock.com

5 comments

  1. Hey Thomas, I like your posts and almost read all of them 🙂 Before assign the datasource of tableView, when we assign the array that tableViewDataSource will use, we don’t need to reload the tableView.

    //Just like that:
    dataSource.movies = [“Terminator”,”Back To The Future”,”The Dark Knight”]
    tableView.dataSource = dataSource

  2. Hi Thomas! It is a good practice, but you can do even better by creating separate object to encapsulate your data. Something like MoviesManager. Right now your DataSource class is doing too much.

    MoviesManager could look like this:

    class MoviesManager {

    private var movies = [String]()

    public var moviesCount: Int { return movies.count }

    public func movie(at index: Int) -> String {

    return movies[index]

    }

    }

    And your DataSource class could be change to look like this:

    class DataSource: NSObject, UITableViewDataSource {

    var moviesManager: MoviesManager

    init (manager: MoviesManager) {

    self.moviesManager = manager

    }

    //MARK: – UITableViewDataSource

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    return moviesManager.moviesCount

    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: “cellIdentifier”)!

    cell.textLabel?.text = moviesManager.movie(at: indexPath.row)

    return cell

    }

    }

  3. Hi Thomas! This is great and I loved Levgenii’s suggestion. Question though, what about the delegate?

Comments are closed.