21
Sep
2015
 

Massive View Controllers

by Marcus Zarra

While at Empirical Development and then MartianCraft I had been distanced from all of the inventive solutions that other development teams have been coming up with for the past few years. I was living in a bubble of our development teams.

Now that I am working independently again, I am being exposed to a large number of interesting code bases.

It came as quite a surprise to me that view controllers are considered bad by many developers and that they have been coming up with some rather intersting solutions to make them more “manageable”.

To me, this is an indication that many developers have lost the perspective on what should and what should not be in a view controller. For some reason there is a misconception going around that everything belongs in the view controller.

Madness!

Lets break it down.

Network Code

I have spoken about this at length at conferences around the world.

Putting network code in the view controller is beyond a code smell, it is just wrong.

Where does it go?

All of the network code belongs in NSOperation subclasses. Ideally one NSOperation subclass for each network request. That NSOperation is responsible for creating the network request, receiving the data, parsing it into JSON and storing it in the persistence layer (ideally Core Data).

This creates small, discrete, units of work that are easy to maintain. If a network operation fails, we can isolate the code and find the problem. Further, we can put each call into a unit test very easily and run it in isolation until it is perfect.

But where do we fire these operatons from? I strongly recommend creating a top level data controller that is responsible for maintaining a reference to the persistence engine (again, ideally Core Data). This top level data controller is normally instantiated in the application’s delegate and is passed down to the view controllers via dependency injection.

Using a singleton for this is bad; another code smell. Why is a subject for another discussion.

What does this look like?

The data controller starts out very simply:

import UIKit
import CoreData

class DataController: NSObject {
    var managedObjectContext: NSManagedObjectContext
    let networkQueue = NSOperationQueue()

    init(completionClosure: () -> ()) {
        //initialize persistence.  NOT LAZY
    }

    func refreshRequest() {
    let op = OperationSubclass()
    op.dataController = self
    op.url = ... //Pass in what is needed
    self.networkQueue.addOperation(op)
    }
}

With this class being passed into the view controller the view controller can react to refresh requests by simply calling:

self.dataController.refreshRequest()    

One line of code that can be wired directly from the button’s action directly to the data controller.

Consolidating the networking code into a central location also adds additional benefits beyond just code isolation. Real time reaction to bandwidth changes, resumability, cancellability, and many other features become trivial to implement. When the network code is in the view controllers the same features are nearly impossible to implement.

How does the view know when the data is ready?

There are a few answers depending on on what kind of view we are dealing with.

The UITableViewController

This is the easiest type of view controller to work with. When we are using Core Data as our persistence engine the view controller practically writes itself.

By putting a NSFetchedResultsController between the data controller and the table view controller, we have extremely brief method implementations.

But what about the table view cells?

Each table view cell should be designed in the storyboard and then populated via a UITableViewCell subclass. No fuss no muss.

With this simple design the population from the view controller can be as easy as:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as! CustomTableViewCell
    let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject

    cell.populateCellFromObject(object)
    return cell
}

Let the view subclass handle populating the labels, images, whatever else is needed to draw the cell.

By keeping the network code out of the view controller and letting the views draw themselves, the view controller becomes very simple. All it really ends up doing is managing the life cycle of the view, exactly what the view controller is meant to do.

Why add more complexity?

Plain View Controllers

Plain view controllers can be a little harder, but generally do not need to be.

The previous view controller is usually responsible for injecting the needed data objects into the view controller. If you aren’t using dependency injection here you are probably doing it wrong.

With the data being injected the view controller just needs to pass that data to the view subclass.

Let the view populate itself from the data!

Let the view controller manage life cycle events.

Build the view in the storyboard.

Use either KVO or core data change notifications to refresh the view. The view controller does not need to handle the data refresh if the data objects do not change.

Use Dependency Injection. The UIKit framework was designed for it. When you avoid it you are fighting the frameworks and just making everything harder on yourself.

Duplicating data

A few of the interesting design patterns that I have seen recently involve duplicating the data in memory. Sometimes there are two copies of the data, one in the cache (aka persistence layer) and another for the views.

This defeats some of the most amazing pieces of Cocoa!

When our UI has a single copy of a data object we can observe changes on that data object.

We can use KVO (Key Value Observing) or we can use notifications from Core Data to detect changes to the data to react and refresh.

Why is this important?

We do not need to write code to watch the cache!

Our network layer simply updates the cache and the view will update itself. We completely avoid tight coupling between the network layer and the view layer.

Our code base gets smaller.

Our code base gets more maintainable.

Our code base gets faster.

When we duplicate the data and/or try to introduce other design patterns into UIKit based applications we are adding unnecessary complexity.

Why subclass the view?

The UIView class is meant to be subclassed. The documentation on this class is well defined and subclasses of UIView integrate extremely well into the UIKit framework.

When the UIView is subclassed and the data is injected into the view, the view can be reused. This is the core of the concept of code reuse. If a piece of data is going to be displayed in multiple places then it makes sense that there should be a view to be reused.

What about data validation?

When we subclass the view and the view is aware of the data, guess where the data validation goes?

In the view subclass!

Why? So that the view can respond to the validation failures!

This is a big difference between OS X and iOS. In OS X, Cocoa Bindings allow the view elements to query the data directly and confirm validation. This is a very cool feature that makes validation borderline magic.

But Cocoa Bindings do not exist on iOS. Therefore we must validate the data as close to the editing view as possible. This allows us a very short path back to the user to notify the user that the data is invalid.

What better place than in the view itself?

If we do this at the persistence layer, it is too late. At best it will cause a tight coupling between the persistence layer and the view layer. A very bad thing. At worst the user will be in a bad state. Unable to save their data and forced to go find the edit view again that is probably off screen, deallocated, gone. A terrible user experience.

When we design our user inteface to have data validation feedback it then becomes trivial to validate the data upon entry and give the user immediate feedback.

What about business logic?

What is business logic?

Business logic is code that is not part of putting data on the screen and is not part of receiving data from the network (generally).

If the business logic is about posting something to a server, then it belongs in the network layer!

If the business logic is about controlling a device, then it belongs in a manager for that device.

Define what your business logic is and then determine where it goes. Very rarely does it belong in the view controller.

Doesn’t this make the views heavy?

No.

The code to populate the view must live somewhere. If we put it in the view controller then the view controller gets too big.

If we create an object to sit between the view and the view controller then we are creating unnecessary additional objects.

The view is already holding onto references to the elements contained in the view that are provided from the storyboard, it makes perfect sense to put the population code with the references to what is being populated.

This does not make our code any heavier than any other design. The code is arguably a class lighter per view.

What is left over

When we remove the view population and the data code from the view controller; the view controller is trimmed down dramatically. Now the view controller is back down to doing its job, view lifecycle events.

Even the view lifecycle events have been dramatically reduced over the past few years with the introduction of storyboards.

Wrap up

We must never forget the K.I.S.S. principal. It absolutely applies to iOS and OS X development. When we introduce multiple layers of indirection, multiple copies of the data, we are adding completely unnecessary complexity to the application.

That complexity will cost us. It will cost us CPU, Battery Life, Memory, and maintainability.

Why do it the hard way?

About the Author

Marcus S. Zarra is best known for his expertise with Core Data, persistence and networking. He has been developing Cocoa applications since 2004 and has been developing software for most of his life.Teaching at CocoaConf

There are very few developers who have worked in more environments, on more projects or with more teams than Marcus has.

Marcus is currently available for short to medium term development contracts, code reviews and workshops.

If your team is struggling with code structure, networking, or persistence please contact him. He would love to help your team produce the best application possible.

Marcus can be reached via email at marcus@cimgf.com.

Comments

[…] few days ago I read this post about Massive View Controllers by Marcus Zarra. Marcus and I have been friends for years. I’ve probably learned more about iOS […]