The Model View Controller (MVC) design pattern is no stranger to iOS developers. Every iOS application uses the MVC pattern to distribute the responsibilities of the application flow. Unfortunately, most of the time, this distribution ends up with a lot of code in the controllers of the application. These bloated controllers are responsible for creating technical debt and also create maintenance nightmares for the future. In this article, you will learn how to tame these monsters. I'll start with an app that consists of a massive controller and work my way toward a leaner implementation.

The basic principle behind lean controllers is the single responsibility principle. This means that every component of the application should have a single job and purpose. By isolating each component to perform a single task, you make sure that the code remains lean and easy to maintain.

Scenario

The iOS application under discussion is a simple user task-management app. The application consists of a single screen where the user can add tasks to a list. Users can delete the tasks by swiping right to left on the row and then pressing the delete button. The user can mark the tasks as complete by selecting the cell using a single tap gesture. All of the tasks are persisted in the SQLITE database using the FMDB wrapper. The screenshots of the app running in action is shown in Figure 1 and Figure 2.

Figure 1: TODO app in action
Figure 1: TODO app in action
Figure 2: Entering new tasks in the TODO app
Figure 2: Entering new tasks in the TODO app

In the first section, you'll inspect the TasksTableViewController implementation, which orchestrates the complete flow of the application. You'll quickly realize that the implemented code is not reusable and is hard to maintain. You'll take the whole application apart and create reusable, single-responsibility components that protect the application for future changes.

The concepts and techniques learned in this article can be applied to any iOS application regardless of the data access layer used.

Implementing a Lean View Controller

The best place to start is the viewDidLoad event of the TasksTableViewController. The viewDidLoad event is invoked each time the app runs. Inside the viewDidLoad event, you trigger the initializeDatabase function. The initializeDatabase function is responsible for copying the database from the application bundle to the documents directory of the application. Because this procedure needs to be performed only once for each app, there's no need to call it from the viewDidLoad event. You're going to move the initializeDatabase function into AppDelegate where it can be called from within the didFinishLaunchingWithOptions event. By moving the initializeDatabase call inside the didFinishLaunchingWithOptions, you've made sure that initializeDatabase is called only once during the lifecycle of the application. Listing 1 shows the implementation of the initializeDatabase function.

Listing 1: InitializeDatabase function to setup the database

func application(application: UIApplication, 
   didFinishLaunchingWithOptions launchOptions:
   [NSObject: AnyObject]?) -> Bool {

    initializeDatabase()

    return true
}


private func initializeDatabase() {

    let documentPaths =
        NSSearchPathForDirectoriesInDomains
        (NSSearchPathDirectory.DocumentDirectory, 
        NSSearchPathDomainMask.UserDomainMask, true)

    guard let documentDirectory = documentPaths.first else {
        return }

    self.databasePath = documentDirectory
        .stringByAppendingPathComponent
        (self.databaseName)

    let fileManager = NSFileManager.defaultManager()

    let success = fileManager.fileExistsAtPath(self.databasePath)

    guard let databasePathFromApp = NSBundle
        .mainBundle().resourcePath?
        .stringByAppendingPathComponent
        (self.databaseName) else {
            return
    }

    print(self.databasePath)

    if success {
        return
    }

    try! fileManager.copyItemAtPath(databasePathFromApp,
        toPath: self.databasePath)

}

Next, move to the cellForRowAtIndexPath function. At first glance, the cellForRowAtIndexPath implementation doesn't show any warning signs, but the devil's in the details. The properties associated with the cell are assigned inside the function. In the future, you might be interested in displaying some additional properties on the user interface. This means that you'll assign more cell properties inside the cellForRowAtIndexPath function, hence polluting it with unnecessary code, as indicated in Listing 2.

Listing 2: Cell properties assigned inside the cellForRowAtIndexPath function

override func tableView(tableView: UITableView, 
    cellForRowAtIndexPath indexPath: NSIndexPath)
    -> UITableViewCell {

    guard let cell = tableView.dequeueReusableCellWithIdentifier
        ("TaskTableViewCell", forIndexPath: indexPath)
        as? TaskTableViewCell else {
            fatalError("TaskCell not found")
        }

    let task = self.tasks[indexPath.row]
        cell.titleLabel.text = task.title;
        cell.subTitleLabel.text = task.subTitle;
        // Future properties polluting code
        cell.imageURL = task.imageURL
        //  Future properties polluting code

        return cell
}

At first glance, the cellForRowAtIndexPath implementation doesn't show any warning signs, but the devil is in the details

The best way to deal with this problem is to refactor the configuration of the cell into a separate function. Luckily, the TaskTableViewCell is a perfect candidate to define such a function. The implementation of the configureCell function is shown here:

func configure(task :Task) {
    self.titleLabel.text = task.title
    self.shortTitle.text = task.shortTitle
    self.imageURL = task.imageURL
}

The configure function accepts a Task model object and then populates the cell properties based on the properties of the model. The next snippet shows the call to the configure function inside the cellForRowAtIndexPath event.

override func tableView(tableView:
    UITableView, cellForRowAtIndexPath
    indexPath: NSIndexPath)
    -> UITableViewCell {

        guard let cell = tableView.
            dequeueReusableCellWithIdentifier
            ("TaskTableViewCell", forIndexPath: indexPath)
            as? TaskTableViewCell else {
                fatalError("TaskCell not found")
            }
            let task = self.tasks[indexPath.row]
            cell.configure(task)
            return cell
    }

Implementing TasksDatasource

In the current implementation, the data source is tied to the TasksTableViewController. This means that if you have to use the same data in a different controller, you have to manually copy and paste the code into a new controller. As a developer, you realize that copy/pasting is the root of all evil and that it contributes to the technical debt. Instead, you will refactor the data source into a separate TasksDataSource class.

The TasksDataSource class implements the UITableViewDataSource protocol. The TasksDataSource initializer consists of the following parameters:

  • cellIdentifier: A string instance representing the UITableViewCell unique identifier
  • items: An array of task instances
  • tableView: The UITableView instance

The TasksDataSource class also provides the implementations for the cellForRowAtIndexPath and numberOfRowsInSection functions. Now, the TasksTableViewController can call the setupTableView function from inside the viewDidLoad event to initialize the TasksDataSource, as shown in Listing 3.

Listing 3: Setting up UITableViewDataSource

func setupTableView() {

    // get the data from the database
    let db = FMDatabase(path: self.databasePath)

    db.open()

    let result = db.executeQuery("select * from tasks",
        withArgumentsInArray: [])

    while(result.next()) {

        let task = Task(title: result.
            stringForColumn("title"))
        // add to the collection
        self.tasks.append(task)
    }

    // close the connection
    defer {
        db.close()
    }

    // initialize the data source
    self.dataSource = TasksDataSource
        (cellIdentifier: "TaskTableViewCell", items:
        self.tasks,tableView: self.tableView)
        self.tableView.dataSource = self.dataSource

        self.tableView.reloadData()
}

Apart from eliminating the unnecessary code implemented in TasksTableViewController, TasksDataSource also helps to create a reusable data source that can be unit tested and used in different parts of the application.

The setupTableView also exposes another problem related to data access. Currently, the data access code is littered throughout the TasksTableViewController, which not only makes it harder to test but also harder to reuse and maintain. In the next section, you'll refactor the data source into a separate service, which allows more flexibility in the future.

Implementing TasksDataService

TasksDataService will be responsible for all of the CRUD (Create, Read, Update, Delete) operations related to the application. TasksDataService maintains the communication bridge between the view controller and the persistent storage system, which, in this case, is SQLite3. The first step is to initialize the TasksDataService, which also sets up the FMDatabase instance, as shown in the next snippet.

var db :FMDatabase!

init() {

    var token :dispatch_once_t = 0
    dispatch_once(&token) {
        let appDelegate = UIApplication.sharedApplication().delegate
            as! AppDelegate
        let databasePath = appDelegate.databasePath
        self.db = FMDatabase(path: databasePath)
    }
}

Next, you'll implement the GetAll function that will be responsible for retrieving all the tasks from the database. The GetAll implementation is shown in Listing 4.

Listing 4: GetAll function to retrieve all tasks

func getAll() -> [Task] {

    var tasks = [Task]()

    db.open()

    let result = db.executeQuery("select * from tasks",
        withArgumentsInArray: [])

    while(result.next()) {

        let task = Task(title: result.stringForColumn("title"))
        // add to the collection
        tasks.append(task)
    }


    defer {
        db.close()
    }

    return tasks

}

The congested setupTableView function can be replaced with the new leaner implementation, as shown in Listing 5.

Listing 5: The setupTableView function utilizing the TasksDataService

func setupTableView() {

    self.tasksDataService = TasksDataService()
    self.tasks = self.tasksDataService.getAll()

    // initialize the data source
    self.dataSource = TasksDataSource(cellIdentifier:
        "TaskTableViewCell", items: self.tasks,
        tableView: self.tableView)
    self.tableView.dataSource = self.dataSource

    self.tableView.reloadData()
}

You can apply the same approach to the save and delete operations and move the implementations in the TasksDataService class. This eliminates a lot of code from the TasksTableViewController and moves you one step closer toward Lean Controller implementation. In the next section, you're going to look at the tableView delegate events and how they can be refactored into their own classes.

Extending TasksTableViewController

Swift 2.0 introduced the concept of Protocol Extensions, which allowed developers to provide default implementations to protocol functions. Currently, the TasksTableViewController contains a lot of code that deals with the look and feel of the UITableView control. This includes the implementations for didSelectRowAtIndexPath and commitEditingStyle events. By providing an extension to the TasksTableViewController, you can move a lot of clutter from the TasksTableViewController class to the extension.

The great thing about extending the TasksTableViewController using protocol extensions is that it will have access to all of the properties defined in the TasksTableViewController implementation. The implementation of the didSelectRowAtIndexPath defined inside the TasksTableViewController extension is shown in Listing 6.

Listing 6: The didSelectRowAtIndexPath implementation inside the TasksTableViewController extension

override func tableView(tableView:
    UITableView, didSelectRowAtIndexPath
    indexPath: NSIndexPath) {

    let task = self.tasks[indexPath.row]

    guard let cell = self.tableView.
        cellForRowAtIndexPath(indexPath) as? TaskTableViewCell else {
            fatalError("Cell does not exist")
        }

    if(!task.isCompleted) {
        cell.titleLabel.attributedText = task.title.strikeThrough()
        task.isCompleted = true
    } else {
        cell.titleLabel.attributedText = task.title.removeStrikeThrough()
        task.isCompleted = false
    }

}

You'll also notice that I've used an extension method for String type to create the strikeThrough effect. This allows you to reuse the strikeThrough function in other parts of the application.

Run the application. On the surface, everything looks fine, but as you strikeThrough the top rows and scroll to the UITableView to the bottom, you'll notice that that your strikeThrough rows are reverted to the default style. The reason is pretty simple: UITableView reuses the cells that it displays. This helps to increase the performance dramatically, as new cells are never created.

In order to fix this issue, you need to edit the configure function of the TaskTableViewCell to include the logic for completed and uncompleted tasks. The implementation is shown in the following snippet.

func configure(task :Task) {
    self.titleLabel.text = task.title
    if(task.isCompleted) {
        self.titleLabel.attributedText = task.title.strikeThrough()
    } else {
        self.titleLabel.attributedText = task.title.removeStrikeThrough()
    }
}

Run the app again and now you'll notice that the strike through rows persists while scrolling the UITableView control!

Conclusion

Implementing lean controllers takes more effort than writing massive controllers. It might be tempting to put all the code into one controller, but keep in mind that although you might move fast initially, you'll quickly hit a wall. It's always better to refactor and move toward a more solid design initially when things are in motion, rather than waiting until the components have been fixed into place.