Taming the Massive Controllers in iOS Part 1

UPDATE: Part 2 is now available

No matter how simple your iOS app may be you still have to conform to the MVC software architecture pattern. The MVC pattern spreads the responsibility of the application among Model, View and Controller. Unfortunately, most of the time the responsibility ends up on the shoulders of the controller. This practice results in what is known as Massive Controllers.

I presented “Taming the Massive Controllers in iOS” at IndieDevStock. If you would like to see my session video then buy the remote pass.

Massive Controllers are view controllers which do not abide by the Single Responsibility Principle. These controllers might be accessing data, calling web services, creating UI elements and doing other tasks which has no direct relation with the controller. This results in technical debt and maintenance nightmares.

The main focus of this post is to show you some techniques that can be used to tame the massive controllers. We will start with a very simple application called “Grocry”. Grocry application is responsible for keeping track of the shopping list. The interface for the Grocry app looks like the following:

Grocry App

Even though the app is extremely simple the code in the designated controller, ShoppingListTableViewController is quite heavy. Here is the fraction of code in segments.

override func viewDidLoad() {
super.viewDidLoad()
initializeCoreDataManager()
// fetch request
let request = NSFetchRequest(entityName: “ShoppingList”)
request.sortDescriptors = [NSSortDescriptor(key: “title”, ascending: true)]
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsController.delegate = self
try! self.fetchedResultsController.performFetch()
}

The initializeCoreDataManager, which is responsible for initializing the CoreData is implemented below:

private func initializeCoreDataManager() {
guard let modelURL = NSBundle.mainBundle().URLForResource(“GrocryDataModel”, withExtension: “momd”) else {
fatalError(“GrocryDataModel not found”)
}
guard let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL) else {
fatalError(“Unable to initialize ManagedObjectModel”)
}
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let fileManager = NSFileManager()
guard let documentsURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first else {
fatalError(“Unable to get documents URL”)
}
let storeURL = documentsURL.URLByAppendingPathComponent(“Grocry.sqlite”)
print(storeURL)
try! persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
let type = NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType
self.managedObjectContext = NSManagedObjectContext(concurrencyType: type)
self.managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
}

The UIAlertView you see in the screenshot is displayed using the following code:

@IBAction func addShoppingListButtonPressed() {
let alertController = UIAlertController(title: “Grocry”, message: “Enter Shopping List”, preferredStyle: .Alert)
let saveAction = UIAlertAction(title: “Save”, style: .Default) { (action :UIAlertAction) in
let shoppingNameTextField = alertController.textFields![0]
self.saveShoppingList(shoppingNameTextField.text!)
}
alertController.addTextFieldWithConfigurationHandler { (textField) in
}
alertController.addAction(saveAction)
self.presentViewController(alertController, animated: true, completion: nil)
}

And the list goes on and on and on..

The code above works without any problem. But looks can be decieving. Every time you add code to the above controller you are creating technical debt. This means everytime you are adding/removing/updating code in the view controller it is going to take longer and longer resulting in headaches down the road.

CoreData Stack

The most obvious refactoring you can perform is to move all the CoreData setup code out of the view controller and into a separate class. We will call that separate class “CoreDataManager” and it’s sole responsibility is to setup the CoreData stack.

class CoreDataManager: NSObject {
var managedObjectObjectContext :NSManagedObjectContext!
override init() {
guard let modelURL = NSBundle.mainBundle().URLForResource(“GrocryDataModel”, withExtension: “momd”) else {
fatalError(“GrocryDataModel not found”)
}
.....  other code to initialize CoreData stack 
}

With CoreData setup moved to a different class we can simply initialize the it from inside the AppDelegate. The implementation is shown below:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// initialize core data manager
let coreDataManager = CoreDataManager()
return true
}

Much cleaner right! Not only that but now you can go in your ShoppingListTableViewController and delete all the code responsible for setting up the CoreData stack. Deleting code is always fun :)

DataSource:

At present all the data source methods are implemented inside the ShoppingListTableViewController. They are the delegate methods of the UITableViewDataSource. These methods include

override func numberOfSectionsInTableView(tableView: UITableView) -> Int
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell 

Instead of implementing the methods inside the ShoppingListTableViewController we can extract them out to a datasource. This way everything related to the datasource will be available in one place and the controller will be free of that responsibility.

ShoppingListDataSource is our new datasource class which is responsible for providing the necessary data to the UITableView control.

class ShoppingListDataSource<CellType :UITableViewCell>: NSObject,UITableViewDataSource, ShoppingListDataManagerDelegate {
var manager :ShoppingListDataManager!
var tableView :UITableView!
var cellIdentifier :String!
private let cellConfigurationHandler :(CellType, ShoppingList) -> ()
init(manager :ShoppingListDataManager,tableView :UITableView, cellIdentifier :String, cellConfigurationHandler :(CellType,ShoppingList) -> ()) {
self.manager = manager
self.tableView = tableView
self.cellIdentifier = cellIdentifier
self.cellConfigurationHandler = cellConfigurationHandler
super.init()
self.manager.delegate = self
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.manager.numberOfSections
}
func shoppingListDataManagerDidInsertShoppingList(indexPath: NSIndexPath) {
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.manager.numberOfItemsInSection(section)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier, forIndexPath: indexPath) as? CellType else {
fatalError(“ShoppingListTableViewCell not defined!”)
}
let shoppingList = self.manager.objectAtIndexPath(indexPath)
self.cellConfigurationHandler(cell, shoppingList)
return cell
}

As you can see in the code above ShoppingListDataSource conforms to the UITableViewDataSource protocol and implement all the required delegate methods.

The most important part of the ShoppingListDataSource is the initialization code which takes in multiple arguments.

init(manager :ShoppingListDataManager,tableView :UITableView, cellIdentifier :String, cellConfigurationHandler :(CellType,ShoppingList) -> ()) {

Apart from the manager argument which is of type ShoppingListDataManager, most are self explanatory. The manager is responsible for communicating with CoreData to retrieve the records and provide it to the datasource.

The ShoppingListTableViewController can now consume the datasource as shown in the implementation below:

class ShoppingListTableViewController3: UITableViewController, {
private var dataSource :ShoppingListDataSource<ShoppingListTableViewCell>!
private var manager :ShoppingListDataManager!
var managedObjectContext :NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
self.manager = ShoppingListDataManager(managedObjectContext: self.managedObjectContext)
self.dataSource = ShoppingListDataSource(manager: self.manager, tableView: self.tableView, cellIdentifier: "ShoppingListTableViewCell", cellConfigurationHandler: { (cell :UITableViewCell, shoppingList :ShoppingList) in
})
self.tableView.dataSource = self.dataSource
}

You can now remove all the code associated with NSFetchedResultsController from your ShoppingListTableViewController since the responsibility is moved to ShoppingListDataSource and ShoppingListDataManager classes.

The ShoppingListDataManager initialization code is shown below:

class ShoppingListDataManager: NSObject, NSFetchedResultsControllerDelegate {
var delegate :ShoppingListDataManagerDelegate!
var managedObjectContext :NSManagedObjectContext!
var fetchedResultsController :NSFetchedResultsController!
init(managedObjectContext :NSManagedObjectContext) {
self.managedObjectContext = managedObjectContext
let request = NSFetchRequest(entityName: ShoppingList.entityName)
request.sortDescriptors = ShoppingList.sortDescriptors
request.predicate = NSPredicate(value: true)
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
self.fetchedResultsController.delegate = self
try! self.fetchedResultsController.performFetch()
}

As indicated previously, ShoppingListDataManager is responsible for communicating with CoreData through NSFetchedResultsController. The only parameter it needs is a reference to the NSManagedObjectContext.

By introducing the ShoppingListDataSource and ShoppingListDataManager we are already witnessing a lot of code improvements. Our ShoppingListTableViewController no longer contains datasource and NSFetchedResultsController delegate methods. The code is much nicer, leaner and easier to understand and change.

In the next part we are going to introduce Swift Extensions and how it can help to create reusable and maintainable code.

Download Code

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.