Articles and podcasts about Swift development, by John Sundell.

Genius Scan SDK

Presented by the Genius Scan SDK

This video has been archived, as it was published several years ago, so some of its information might now be outdated. For more recent articles, please visit the main article feed.

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