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 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.

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.

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.

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.

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

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.

FMDB is a thin Objective-C open source wrapper on top of the SQLite database that provides a lot of helper methods for performing database operations from within your applications.

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.

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.