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.

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 {
        ...
    }
}