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