---
title: 'Lifecycle Methods'
source: 'https://academia.sh/en/courses/uikit/lifecycle-methods'
course: 'UIKit and Bridging'
language: en
updated: '2026-10-21T05:03:15+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

# 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.

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.

```swift
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.

```swift
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.

```swift
// 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.

```swift
// 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 inside `viewWillAppear` should close it inside `viewWillDisappear`.
- **Skipping the `super` call.** 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.

```swift
// 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; `viewDidLoad` is called once, `viewWillAppear` on every appearance.
- Writing a value that changes over time only inside `viewDidLoad` leaves 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.
