Articles, podcasts and news about Swift development, 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.

Emerge

Emerge: Continuously monitor and reduce your app’s size. Emerge’s easy to use plugins for GitHub and fastlane will automatically scan your app’s binary and provide you with simple, actionable suggestions on how to make it smaller and, in turn, faster for your users to download. Set up a demo now!

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