Sunday, March 27, 2016

Avoiding Massive VCs

One of my biggest beefs with iOS development has been the lack of best practices particularly when dealing with what code belongs in the ViewController.

For my development, I like to design with these goals in mind:

  • Each class/method has one responsibility
  • Functions should not have side effects.
  • Well understood functionality should be abstracted away into their own classes.

When writing UIViewControllers, however, I find it tough to adhere to these principles. The reason, I think, is the fact that because the UIViewController essentially is the code for a view in your app it takes on many responsibilities, such as manipulating the view's widgets, holding state for the widgets, setting values on the widgets, and responding to actions taken on a widget.

I see examples of this type of design all over the Internet. This munging of the responsibilities leads to what has been commonly called the "Massive View Controller" design pattern.

There has been a lot written about how to avoid this pattern and why it is a bad thing, so this post may not be very interesting to those who are well versed in this anti-pattern.

For me, though, I have come to a point in my rewrite of my app, Pain Logger, that patterns are starting to emerge to mitigate this issue that I can use for future apps so I thought I would write these up as minimum for my own reference in the future.

If this has provides any value to you I would love to hear about it.

Avoiding Massive View Controllers

To avoid the Massive View Controller problem I have began using four patterns:

  • View Models
  • Unidirectional data flow
  • Services
  • Protocols Extensions

Note: For this discussion when I say "view" I mean the UIViewController as seen on a storyboard along with it's actual UIViewController class representation. My goal is to reduce the code in these view controllers as much as possible to meet the goals I strive for as outlined above.

View Models

The intent of a view model is to hold state for the view. That way when a particular value needs to be displayed on the view, the view asks it's view model for the data rather than storing the data in the view itself. This allows the view model to do two things:

  1. Hold the data in it's raw format
  2. Transform the data into exactly what the view needs to display.

A simple example would be a date. The view model can hold the raw NSDate object and when the corresponding view needs to display the date it can ask the view model for correctly formatted date (i.e. a string).

With this pattern, business logic (in this case formatting of a NSDate into a String) is moved out of the view code and into the view model.

I started my rewrite of Pain Logger thinking I would move all the data manipulation to a view model class for each view controller. In the end that's not exactly how it ended up, but for this discussion I'll keep it simple.

When a view controller first stands up it creates it's corresponding view model.

var viewModel = VC_ViewModel()

Then in the view controller I wire up the widget's action to the control. But the IBAction implementation only updates the view model and calls the method to refresh the UI.

Here is an example where I wired up a UISwitch:

    @IBAction func handleSwitchChanged() {
        viewModel.showGraph = showGraphSwitch.on
        refreshUI()
    }

The refreshUI() method just reads the values from the viewModel and updates the view's controls appropriately. In this case setting the showGraph attribute to true causes another widget in the view to be shown.

This pattern works well for moving the view's state out of the view controller class. But as I worked through this I ran into another pattern that makes it that much more powerful. This pattern is called 'Unidirectional Data Flow'

Unidirectional Data Flow

Unidirectional Data Flow comes from the 'Reactive' world. I first ran into this pattern with my web work using ReactJS. In this pattern a change is made to data via execution of 'actions'. These 'actions' update a central 'store' which then notifies it's listeners that the store was changed.

There are many implementations of this pattern. The one that seems to be getting a lot of traction is the ReSwift project.

One of the problems I ran into with using the 'View Model' pattern was initialization of the state. But when I incorporated that with UDF, things began to clean up a bit.

Now there is a 'global' store that represents the state of the system at any moment in time. The view models no longer hold state for the view, they are responsible for isolating the business logic for formatting the data from the store into the appropriate format for the view.

For example, in Pain Logger, when editing a pain area, a title must be displayed on the view. The view model exposes a computed attribute for the view's title like so:

    var title:String {
        let name = categoryState.categoryName
        if (name.length > 0) {
            return name
        }
        if let cat = category{
            if (cat.isNew()) {
                return "New Pain Area"
            }
        }
        return "Unknown"
    }

In this example categoryState represents the store, so now the view model doesn't hold state, only the store does. When the refreshUI() method is called on the view controller (after being told by the store that it changed) the view model defers to the "store" to get the value of the system state and then provides the view with the correctly transformed data value.

It should be noted that by adding UDF to the design most if not all of the attributes on the view model become computed values. View models now hold no state either. You can see that in the code above where category is used. category is actually another computed value that looks to the store to find the category that is being edited.

Another consequence that occurs by adding UDF is the view controllers now listen to the store for changes, and actions from the view are dispatched to the UDF system rather than calling the refreshUI method directly.

So for example the handleSwitchChanged() method from above becomes:

    @IBAction func handleSwitchChanged() {
        mainStore.dispatch(SetCategoryShowOnGraphAction(showOnGraph:showOnGraphSwitch.on))
    }

In this way most IBAction methods become one liners, very cool.

Services

The next pattern is a little more mainstream. Using this pattern I moved all async operations out to specialized "service" classes. Each "service" uses the singleton pattern (for all of those who don't like this pattern, I still find it useful for this case). Because of the introduction of UDF the service's don't hold state. They just update the stores.

I have services for local and remote data among other things. Since storing to a database should be done off the UI thread I call the correct service for what I want to do. It modifies the database appropriately and updates the store with the changed values after they have been successfully persisted.

Since for the most part these services work asynchronously I can offload the work that needs to be done, then the UDF system kicks in when the response is received.

Protocol Extensions

The final pattern, that I am just getting used to, is protocol extensions. To be honest I didn't fully understand the value of them until I did some more studying of the subject.

I thought that protocols were just like interfaces from Java, and that would be right, until you throw in extensions. That's when things change, A LOT!!

Call me dense or whatever, but it seems to me that only when you start using protocol extensions do you start to get LOTS of reuse.

So the key to this is to define a behavior you want a class to have say like the title property from above. Since the view models don't hold state either I can define a HasTitle protocol like so:

protocol HasTitle {
    var title:String {get}
}

extension HasTitle {
    var title:String {
        let name = categoryState.categoryName
        if (name.length > 0) {
            return name
        }
        if let cat = category{
            if (cat.isNew()) {
                return "New Pain Area"
            }
        }
        return "Unknown"
    }
    
    var category:PLCategory? {
        get {
            return categoryState.selectedCategory
        }
    }
    
    var categoryState:CategoryState {
        get {
            return mainStore.appState.categoryState
        }
    }
}

Now any class can conform to this protocol just by adding it to the class's definition. Because the extension is defined, as well, there is nothing for the class (in this case a view model) to define and it "just gets" the behavior.

This is extremely powerful and I am excited about adding this new found knowledge to my code base.

Conclusion

So there you have it. A brief overview of some of the patterns I have found useful in architecting an iOS app and to avoid the Massive View Controller problem.

Till next time.

No comments:

Post a Comment