Lesson 02 / 15
Lifecycle Methods
Measuring which lifecycle method a value is written in, whether that leaves the screen matching the model or stale, and what moving the write does.
Published Updated
In this lesson
- Lists the lifecycle methods of a view controller and when each is called
- Measures how a value written in a method that is called only once leaves the screen stale
- Shows that moving the write to the method called on every appearance keeps the screen matching the model
- Separates the model's call order, which is the lesson's choice, from the real order, which is not measured
Contents
In the previous lesson the call that wrote the screen was viewDidLoad, and we read it as the screen’s setup. The life of a controller is more than a single setup, though: the view loads, comes onto the screen, leaves the screen, and the same controller can come onto the screen again. Each stage has a lifecycle method that is called for it, and which one we write a value in determines when the screen meets the model.
What this lesson measures is not the order but the consequence of the place of the write: when the same value is written in two different methods, does the screen agree with the model.
The Methods and When They Are Called
The fragment below is the skeleton of an invoice screen. Because it imports the platform framework, it is not run.
import UIKit
final class InvoiceController: UIViewController {
let store: InvoiceStore
private let totalLabel = UILabel()
private let statusLabel = UILabel()
init(store: InvoiceStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
totalLabel.text = store.values["total"] ?? ""
statusLabel.text = store.values["status"] ?? ""
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
}
viewDidLoad is called once, after the controller’s view has been loaded into memory. viewWillAppear is called every time, just before the view comes onto the screen; it runs again when the controller is hidden and shown again. This difference between the two methods carries the whole lesson. The rest of the framework’s call order, that is, the methods that come after appearing and those that concern disappearing, is not measured in this lesson; they are mentioned in prose.
The controller in the skeleton writes the values only inside viewDidLoad. viewWillAppear is overridden but nothing inside it writes to the screen.
The Whole Lifecycle
In the previous section we saw two methods. The order in which the framework calls them is this: the view loads (viewDidLoad), viewWillAppear before coming onto the screen, viewDidAppear after it has come, viewWillDisappear before leaving the screen, and viewDidDisappear after it has left. Besides these there are viewWillLayoutSubviews and viewDidLayoutSubviews, which are called while layout is being computed.
import UIKit
final class InvoiceController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// the view loaded once: set up the subviews
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// before every appearance: write the current value from the model
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// the screen appeared: start work that depends on the user for a while
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// before leaving the screen: write unsaved input to the model
}
}
Each method begins with a super call; if this call is forgotten in an overridden lifecycle method, the superclass’s work is not done. The animated parameter says whether the transition is animated. When the same controller is hidden and shown again, viewDidLoad is not called again; viewWillAppear and viewDidAppear are called on every appearance.
The Measure
The measure is the core of the previous lesson; it is used unchanged.
// Sync.swift — compares every field shown on screen with the model's current value
final class Screen {
let name: String
private(set) var shown: [String: String] = [:]
init(_ name: String) { self.name = name }
func set(_ key: String, _ value: String) { shown[key] = value }
func clear(_ key: String) { shown[key] = nil }
}
func verdict(model: String?, shown: String?) -> String {
switch (model, shown) {
case (nil, nil): return "out-of-scope"
case (nil, _?): return "leftover"
case (_?, nil): return "missing"
case let (wanted?, displayed?): return wanted == displayed ? "matching" : "stale"
}
}
func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
keys.map { key in
let result = verdict(model: model[key], shown: screen.shown[key])
print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") screen=\(screen.shown[key] ?? "-") -> \(result)")
return result
}
}
func countVerdicts(_ verdicts: [String]) {
for name in ["matching", "stale", "missing", "leftover"] {
print("\(name): \(verdicts.filter { $0 == name }.count)")
}
}
Where to Write the Value
The model builds the controller above and a corrected copy of it in plain Swift. This code does not run the framework: the model builds what the code writes to the screen; it is not the framework itself. The appear() method applies an order that the lesson chose: if the view has not loaded, first viewDidLoad, and then viewWillAppear every time.
InvoiceController writes as in the skeleton: both fields only inside viewDidLoad. FixedInvoiceController writes the same two fields inside viewWillAppear. The event is this: the invoice screen opens, the user moves to another screen, the total changes there, and the user comes back. On the return, only viewWillAppear is called.
// main.swift — two places to write, three events
final class InvoiceStore {
private(set) var values: [String: String] = [:]
func update(_ key: String, _ value: String) { values[key] = value }
}
final class InvoiceController {
let store: InvoiceStore
let screen = Screen("InvoiceController")
private var isViewLoaded = false
init(store: InvoiceStore) { self.store = store }
func appear() {
if !isViewLoaded {
isViewLoaded = true
viewDidLoad()
}
viewWillAppear()
}
func viewDidLoad() {
screen.set("total", store.values["total"] ?? "")
screen.set("status", store.values["status"] ?? "")
}
func viewWillAppear() {}
}
final class FixedInvoiceController {
let store: InvoiceStore
let screen = Screen("FixedInvoiceController")
private var isViewLoaded = false
init(store: InvoiceStore) { self.store = store }
func appear() {
if !isViewLoaded {
isViewLoaded = true
viewDidLoad()
}
viewWillAppear()
}
func viewDidLoad() {}
func viewWillAppear() {
screen.set("total", store.values["total"] ?? "")
screen.set("status", store.values["status"] ?? "")
}
}
let store = InvoiceStore()
store.update("total", "120")
store.update("status", "unpaid")
let loadOnly = InvoiceController(store: store)
loadOnly.appear()
let loadedOld = check("loaded-old", model: store.values, screen: loadOnly.screen, keys: ["total", "status"])
let fixed = FixedInvoiceController(store: store)
fixed.appear()
let loadedFixed = check("loaded-fixed", model: store.values, screen: fixed.screen, keys: ["total", "status"])
store.update("total", "150")
loadOnly.appear()
fixed.appear()
let backOld = check("returned-old", model: store.values, screen: loadOnly.screen, keys: ["total", "status"])
let backFixed = check("returned-fixed", model: store.values, screen: fixed.screen, keys: ["total", "status"])
countVerdicts(loadedOld + loadedFixed + backOld + backFixed)
loaded-old | InvoiceController.total: model=120 screen=120 -> matching loaded-old | InvoiceController.status: model=unpaid screen=unpaid -> matching loaded-fixed | FixedInvoiceController.total: model=120 screen=120 -> matching loaded-fixed | FixedInvoiceController.status: model=unpaid screen=unpaid -> matching returned-old | InvoiceController.total: model=150 screen=120 -> stale returned-old | InvoiceController.status: model=unpaid screen=unpaid -> matching returned-fixed | FixedInvoiceController.total: model=150 screen=150 -> matching returned-fixed | FixedInvoiceController.status: model=unpaid screen=unpaid -> matching matching: 7 stale: 1 missing: 0 leftover: 0
On the first load both controllers are matching: both write the values on the first appearance. The difference shows on the return. When the total changes, viewDidLoad is not called again; the screen of InvoiceController stays at 120, the model says 150, and the line comes out stale. The status field never changed, so it stays matching in the same controller. Same field, same event, the only difference is the place of the write: FixedInvoiceController writes on every appearance, so it shows 150.
The place of the write is a choice. viewDidLoad is the place for work that is done once: setting up subviews, connecting targets. The place for a value that is read from the model, written to the screen, and may change over time is the method called on every appearance. A model value that changes while the screen is open cannot be caught by appearing; this lesson does not measure it.
VC1. In the model, appear() loads the view only on the first call and runs viewWillAppear on every call. This is the lesson’s summary of the framework’s behavior; the framework’s own call order, which method is called first, or the number of calls was not measured.
Common Mistakes
- Writing the current value only inside
viewDidLoad. The controller loads once, but by the time we return from another screen the model may have changed. On the return the screen shows the old value; this is what the lesson measures. - Putting work that should not repeat on every appearance inside
viewWillAppear. Setup work, for example adding a subview or opening a record, runs again on every appearance. A controller that opens a record insideviewWillAppearshould close it insideviewWillDisappear. - Skipping the
supercall. The superclass does its own work in these methods; with no call there is no work. - Trusting the view’s size inside
viewDidLoad. At that point the view’s bounds are not yet final. A calculation that depends on size belongs to the layout methods. - Waiting on a long task inside
viewWillAppear. The screen’s appearing depends on that method returning.
The first item is the lesson’s measurement: the same value can be written in two different methods, and only one of them keeps the screen matching the model.
Reading the Measurement
The two controllers read the same store, write the same fields, and meet the same events. The only difference between them is the place of the write. Of the measurement’s eight lines, seven are matching and one is stale; the stale line is the state, after the return, of the controller that writes only inside viewDidLoad. The corrected controller is matching on the same event.
This result should not be read as a reason to call one method “the right place”. If a value is read once and will not change, viewDidLoad is enough, and rewriting it on every appearance would be needless repetition. The measurement says only this: if the model changes between appearances and the write is only at load, the screen stays stale. Knowing which fields can change between appearances is the precondition for choosing the place of the write.
Layout Methods and Choosing the Place of the Write
viewDidLayoutSubviews is called when the layout of the views has been computed, and this does not happen once: it is called again when the screen rotates, when the size changes, or when the size of the subviews changes. The job of this method is to look at the final measurements and do a calculation; one-time work such as adding a subview or opening a record is not put here, because it would be repeated on every call.
Three questions are enough to choose where to write a value:
- Is the value read once and does it stay constant for the whole life of the screen? Then it is written inside
viewDidLoad. - Could the value have changed while the screen stayed behind another screen and came back? Then it is written inside
viewWillAppear; this way the screen is current before it appears. - Does the value change through a user action while the screen is visible? Then the write is made inside that action itself; lifecycle methods do not catch this change.
The third case is left to this course’s lesson on outlets and actions. For now, what matters is the difference between the first two: both set up the screen, but one runs only on the first load and the other on every appearance.
Child controllers also receive these calls: when a controller is placed inside another one, the lifecycle calls are passed on to it too, and isMovingToParent says at that moment that the controller has just been added. The measurement in this lesson stays limited to a single controller; the child controller relationship is not measured.
Things to Watch in the Call Order
The superclass version of every lifecycle method should be called. If the super.viewWillAppear(animated) line is skipped, the work the superclass does is left half done, and this mistake often shows up only on certain screens, after a certain order. The position of the call matters too: it is a common habit to put the superclass call at the start in setup work and at the end in teardown work.
Heavy work should not be done inside these methods. viewWillAppear makes the screen wait before appearing; if there is a long read inside it, the transition stutters. Long work is moved elsewhere, and the screen is written when the result arrives. When the result arrives the screen may no longer be visible; in that case the write call is still safe, because the view tree stands as long as the controller lives, but the effort is wasted.
Finally, calling these methods by hand is wrong. The caller is the system; a controller calling its own viewDidLoad in code causes the view to be set up twice. When the screen needs to be rewritten, the right way is to put the writing work in a separate method and call that.
The Bounding Measurement
The call order of the model is given by main.swift; the framework does not give it. The block below makes this visible: the same controller, with a call log, and with the model changed before the first appearance.
// main.swift — the call order is the lesson's choice
final class InvoiceStore {
private(set) var values: [String: String] = [:]
func update(_ key: String, _ value: String) { values[key] = value }
}
final class LoggedInvoiceController {
let store: InvoiceStore
let screen = Screen("LoggedInvoiceController")
private(set) var calls: [String] = []
private var isViewLoaded = false
init(store: InvoiceStore) { self.store = store }
func appear() {
if !isViewLoaded {
isViewLoaded = true
viewDidLoad()
}
viewWillAppear()
}
func viewDidLoad() {
calls.append("viewDidLoad")
screen.set("total", store.values["total"] ?? "")
}
func viewWillAppear() {
calls.append("viewWillAppear")
}
}
let store = InvoiceStore()
store.update("total", "120")
let controller = LoggedInvoiceController(store: store)
store.update("total", "130")
controller.appear()
controller.appear()
print(controller.calls.joined(separator: ", "))
let early = check("before-appearing", model: store.values, screen: controller.screen, keys: ["total"])
countVerdicts(early)
viewDidLoad, viewWillAppear, viewWillAppear before-appearing | LoggedInvoiceController.total: model=130 screen=130 -> matching matching: 1 stale: 0 missing: 0 leftover: 0
Here the model changed after the controller was built but before it appeared, and viewDidLoad read the value of that moment; the line is matching. The call log that was written is not a measurement: appear() wrote that order. The real framework’s order is not read from this block; it is read from the skeleton fragment and the prose.
Summary
- Lifecycle methods are calls tied to the stages of a controller’s life;
viewDidLoadis called once,viewWillAppearon every appearance. - Writing a value that changes over time only inside
viewDidLoadleaves the screen stale when the model changes while the screen sits under another screen. - Moving the same write to the method called on every appearance keeps the screen matching the model on the same event.
- A field that never changes stays matching in both places; the difference shows up only in the field that changes.
- The call order in the model is the lesson’s choice; the framework’s real order is not measured.
- This lesson’s agreement: stale.
Next Step
In this lesson the return to the screen was a single event. What really produces the return is a screen being placed on top of another screen and later removed. In the next lesson we will build navigation controllers and measure whether a selection that was removed from the model stays on the screen when a screen is popped off the stack.
To keep your progress and take notes, Log in
My notes
Log in to take notes.