Swift clip: Controllers in MVC
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 {
...
}
}