Weekly Swift articles, podcasts and tips by John Sundell.

Logic controllers in Swift

Published on 06 May 2018

One big challenge that most Swift developers face from time to time, is how to deal with Massive View Controllers. Whether we're talking about subclasses of UIViewController on iOS & tvOS or NSViewController on the Mac, this type of classes tend to grow very large - both in terms of scope and number of lines of code.

The problem with many view controller implementations is that they simply have too many responsibilities. They manage views, perform layout and handle events - but also manage networking, image loading, caching, and many other things. Some may argue that this problem is inherent to how the MVC design pattern is structured - that it encourages massive controller classes, since they are the center point between the view and the model.

While architectures other than Apple's default of MVC certainly have their place, and can in many situations be a great tool in order to break down large view controllers, there's also many ways that this problem can be solved without completely switching architecture. This week, let's take a look at one such way - using Logic Controllers.

The examples in this post will use UIViewController and iOS-based code, but can also be applied when dealing with NSViewController on macOS.

Composition vs extraction

In general, there are two approaches that we can take when we want to split up a large type into multiple pieces - composition and extraction.

Using composition we can combine multiple types together to form new functionality. Instead of creating large types that have several responsibilities, we create more modular building blocks that can be combined to give us the features that we need. For example, in "Using child view controllers as plugins in Swift" we used composition to create small, reusable view controllers that can easily be plugged into others. Here's a code sample from that post where we add a LoadingViewController as a child in order to display a loading indicator:

class ListViewController: UITableViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadItems()
    }

    private func loadItems() {
        let loadingViewController = LoadingViewController()
        add(loadingViewController)

        dataLoader.loadItems { [weak self] result in
            loadingViewController.remove()
            self?.handle(result)
        }
    }
}

While creating general-purpose, composable view controllers is great for things like loading views and reusable lists, it's not a silver bullet. Sometimes breaking a view controller down into small, modular children is not very practical - and can add a lot of complexity for very little gain. In situations like this, extracting functionality into a separate, dedicated type can be a better option.

Using extraction we can pull out parts of a large type into a separate type that still is tightly coupled with the original. This is something that several architectural patterns focus on - including things like MVVM (Model-View-ViewModel) and MVP (Model-View-Presenter). In the case of MVVM, a View Model type is introduced to handle much of the Model->View transformation logic - and when using MVP a Presenter is used to contain all of the view presentation logic.

While we'll take a closer look at both MVVM and MVP (along with other architectural patterns) in future posts, let's take a look at how extraction can be used while still sticking with MVC.

Logic and views

One thing that makes the ViewController type a bit awkward is that it kind of belongs both to the view layer and the controller layer at the same time (I mean, it's right there in the name; ViewController). But just like how child view controller composition has shown us that there's nothing stopping us from using multiple view controllers to form a single UI, there's really nothing stopping us from having multiple controller types either.

One way to do that is to split a view controller up into a view part and a controller part. One controller will remain a subclass of UIViewController and contain all the view-related functionality, while another controller can become decoupled from the UI itself and instead focus on handling our logic.

For example, let's say we're building a ProfileViewController, that we'll use to display the current user's profile in our app. It's a relatively complex piece of UI, in that it needs to perform several different tasks:

If we were to put all of the above functionality into the ProfileViewController type itself, we pretty much know that it's going to end up becoming quite massive and complicated. Instead, let's create two controllers for our profile screen - a ProfileViewController and a ProfileLogicController.

Logic controllers

Let's start by defining our logic controller. It's API is going to consist of all the actions that can be performed in our view, and for each action a new state is returned as part of a completion handler. That means that our logic controller can become more or less stateless, which means it's going to be a lot easier to test. Here's what our ProfileLogicController will end up looking like:

class ProfileLogicController {
    typealias Handler = (ProfileState) -> Void

    func load(then handler: @escaping Handler) {
        // Load the state of the view and then run a completion handler
    }

    func changeDisplayName(to name: String, then handler: @escaping Handler) {
        // Change the user's display name and then run a completion handler
    }

    func changeProfilePhoto(to photo: UIImage, then handler: @escaping Handler) {
        // Change the user's profile photo and then run a completion handler
    }

    func logout() {
        // Log the user out, then re-direct to the login screen
    }
}

As you can see above, the state type for our profile screen is called ProfileState. It's what we'll use to tell our ProfileViewController what to render. We'll use the technique from "Modelling state in Swift" and create an enum with distinct cases for each state, like this:

enum ProfileState {
    case loading
    case presenting(User)
    case failed(Error)
}

Each time an event happens in the UI, our ProfileViewController will call ProfileLogicController in order to handle that event and return a new ProfileState for the view controller to render. For example, when the profile view is about to appear on the screen, we'll call load() on the logic controller in order to retrieve the view's state - which we'll then render:

class ProfileViewController: UIViewController {
    private let logicController: ProfileLogicController

    init(logicController: ProfileLogicController) {
        self.logicController = logicController
        super.init(nibName: nil, bundle: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        render(.loading)

        logicController.load { [weak self] state in
            self?.render(state)
        }
    }
}

We can now put all the logic related to loading the view's state inside our logic controller, rather then having to mix it with our view setup and layout code. For example, we might want to check if we have a cached User model that can simply be returned, or otherwise load one over the network - like this:

class ProfileLogicController {
    func load(then handler: @escaping Handler) {
        let cacheKey = "user"

        if let existingUser: User = cache.object(forKey: cacheKey) {
            handler(.presenting(existingUser))
            return
        }

        dataLoader.loadData(from: .currentUser) { [cache] result in
            switch result {
            case .success(let user):
                cache.insert(user, forKey: cacheKey)
                handler(.presenting(user))
            case .failure(let error):
                handler(.failed(error))
            }
        }
    }
}

The beauty of this approach is that our view controller doesn't need to know how its state was loaded, it simply needs to take whatever state the logic controller gives it and render it. By decoupling our logic from our UI code, it also becomes a lot easier to test. To test the above load method, all we'd have to do is mock the data loader and the cache and assert that the correct state is returned in the cached, success and error situations.

To learn more about writing unit tests containing asynchronous code, like above, check out "Unit testing asynchronous Swift code".

Simply a renderer

Every time a new state was loaded, we call the view controller's render() method to render it. This enables us to treat our view controller more or less like a simple renderer, by reactively handling each state as it comes in, like this:

private extension ProfileViewController {
    func render(_ state: ProfileState) {
        switch state {
        case .loading:
            // Show a loading spinner, for example using a child view controller
        case .presenting(let user):
            // Bind the user model to the view controller's views
        case .failed(let error):
            // Show an error view, for example using a child view controller
        }
    }
}

Just like how we called load() on the logic controller when the view controller is about to appear on the screen, we can use the same pattern when handling UI events - like when the user enters a new display name into a text field. Here we'll notify the logic controller (which in turn can call our server to update the user's display name) and render the new, updated state:

extension ProfileViewController: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        guard let newDisplayName = textField.text else {
            return
        }

        logicController.changeDisplayName(to: newDisplayName) {
            [weak self] state in
            self?.render(state)
        }
    }
}

Regardless of what type of event the view controller is handling, it performs the same two actions: Notify the logic controller, and render the resulting state. As such the view controller ends up having a very similar relationship to its logic controller as when splitting up a web site into front end (browser) and back end (server) components. Each side can focus on what they do best.

Conclusion

Extracting the core logic of a view controller into a matching logic controller can be a great way to avoid the Massive View Controller problem, while still sticking to the MVC pattern. Granted, several of the concepts used for this technique are similar as when - for example - applying view models using MVVM, and in future articles we'll take a look at the differences and similarities between these approaches.

Regardless of how we slice up our view controllers - be it using child view controllers, dedicated UIView subclasses, view models, presenters or logic controllers - the goal remains the same, to let our UIViewControllers focus on doing what they do best - controlling views.

What approach that will be the most appropriate for your app depends a lot on your requirements, and like always I recommend experimenting with multiple techniques to find out which one suits your needs the best. It's also important to note that not all view controllers need to be broken down - some screens might be so simple that using a single view controller for it may do the job just fine.

What do you think? Have you tried using logic controllers before, or is it something you'll try out? Let me know - along with any questions, comments or feedback you might have - on Twitter @johnsundell.

Thanks for reading! 🚀