Weekly Swift articles, podcasts and tips by John Sundell.

Swift clip: Controllers in MVC

Published on 28 Apr 2020

Let’s take a look at the role that controllers play within the MVC design pattern, and how we can avoid some of the most common issues when working with them — particularly around how we can break up Massive View Controllers into smaller building blocks.

Paw

This ad keeps all of Swift by Sundell free for everyone. If you can, please check this sponsor out, as that directly helps support this site:

Paw

Paw: A GraphQL and REST API client that lets you test and describe the APIs that you call from your app. Just enter the URL of the API endpoint that you’re looking to call, add any headers, parameters, authentication, or body data. Hit return — and everything is automatically checked for you, from the standard OAuth 2 login to very custom API flows.

Links

Sample code

An example of a view controller that constructs its header view inline within its loadView method:

class ProfileViewController: UIViewController {
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        let headerView = UIView()
        ...
        view.addSubview(headerView)
    }
}

Moving that header view implementation to a new view controller instead:

class HeaderViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
    }
}

Embedding our new HeaderViewController as a child view controller:

class ProfileViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let headerVC = HeaderViewController()
        view.addSubview(headerVC.view)
        addChild(headerVC)
        headerVC.didMove(toParent: self)
        ...
    }
}

Extending UIViewController with an API to make it easier to add child view controllers:

extension UIViewController {
    func add(_ child: UIViewController) {
        addChild(child)
        view.addSubview(child.view)
        child.didMove(toParent: self)
    }
}

You can also find a remove equivalent to the above API within the Basics article about child view controllers.

An example of a view controller method that doesn’t really have much to do with controlling a view:

class ProfileViewController: UIViewController {
    private let userID: User.ID
    private let networking: NetworkManager
    ...

    private func loadUser() {
        let endpoint = Endpoint.user(id: userID)

        networking.request(endpoint) { [weak self] result in
            do {
                let data = try result.get()
                let user = try JSONDecoder().decode(User.self, from: data)
                self?.render(user)
            } catch {
                self?.showErrorView(for: error)
            }
        }
    }

    ...
}

Implementing a generic ViewState enum that we can use to model any view controller’s high-level rendering state:

enum ViewState<Model> {
    case loading
    case presenting(Model)
    case failed(Error)
}

Implementing a logic controller companion for our ProfileViewController:

class ProfileLogicController {
    private let userID: User.ID
    private let networking: NetworkManager

    ...

    func loadCurrentState(then handler: @escaping (ViewState<User>) -> Void) {
        let endpoint = Endpoint.user(id: userID)

        networking.request(endpoint) { result in
            do {
                let data = try result.get()
                let user = try JSONDecoder().decode(User.self, from: data)
                handler(.presenting(user))
            } catch {
                handler(.failed(error))
            }
        }
    }
}

Using our new logic controller within our view controller:

class ProfileViewController: UIViewController {
    private let logic: ProfileLogicController

    ...

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

        logic.loadCurrentState { [weak self] state in
            self?.render(state)
        }
    }
    
    private func userDidPickNewProfileImage(_ image: UIImage) {
        logic.handleNewProfileImage(image) { [weak self] state in
            self?.render(state)
        }
    }
}

An example of a model that’s currently managed as a singleton:

struct Player {
    var name: String
    var score: Int
    var challenges: [Challenge]
    ...
}

extension Player {
    static var current: Player?
}

Using a model controller to manage our model instead:

class PlayerModelController {
    private(set) var model: Player

    init(model: Player) {
        self.model = model
    }
    
    func levelCompleted(_ level: Level) {
        var levelScore = level.enemiesDefeated * 100
        levelScore += level.obstaclesAvoided * 50
        levelScore *= level.difficulty.scoreMultiplier

        model.score += levelScore
    }
    
    func observe(using closure: @escaping (Player) -> Void) -> Cancellable {
        ...
    }
}