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.

Architecting SwiftUI apps with MVC and MVVM

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:

Architecting SwiftUI apps with MVC and MVVM

Architecting SwiftUI apps with MVC and MVVM: Although you can create an app simply by throwing some code together, without best practices and a robust architecture, you’ll soon end up with unmanageable spaghetti code. Learn how to create solid and maintainable apps with fewer bugs using this free guide.

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