---
title: 'UIKit and Bridging'
source: 'https://academia.sh/en/courses/uikit'
language: en
updated: '2026-10-21T05:03:15+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Manages the view controller lifecycle'
    - 'Builds screens with constraint-based layout'
    - 'Uses two interface frameworks together in the same app'
    - 'Distinguishes whether a field shown on screen is matching, stale, missing or leftover relative to the model'
    - 'Builds delegate and target-action links with a small model and recognizes that the model is not the framework itself'
---

# UIKit and Bridging

In an imperative interface, code updates the screen when the model changes; code that forgets the update does not crash, the screen drifts away from the model. The course measures every mechanism with a single question — does this screen agree with the model? Is the shown value the same as the model, an old one, never written at all, or left over from an earlier value? View controllers and their lifecycle, navigation and presentation, outlets and actions, constraint-based layout, stack, table and collection views, the delegate pattern, drawing and layer animation, and using two interface frameworks together in the same app all pass through the same frame.

## View Controllers

The view controller as a per-screen responsibility, lifecycle methods across the loading, appearing and disappearing stages, stack-based navigation controllers, modal and push presentation styles, a comparison of building interfaces with a visual editor and in code, and the outlets and actions that link the interface to code.

### The View Controller Concept

Source: https://academia.sh/en/courses/uikit/the-view-controller-concept

How a view controller carries a screen between the model and the view, and an introduction to the measurement that classifies whether a screen agrees with the model as matching, stale, missing or leftover.

In an earlier course (M30/K03), a declarative framework had us read why a view was recomputed through a trigger; when a value changed, the framework updated the screen by itself. This course gives that work back to the programmer. In an imperative framework the model changes, and updating every label and every row is left to the code that is written. Forgetting to write an update raises no error; the screen keeps showing the old thing.

For that reason the question of the course is not a trigger but an outcome: **does this screen agree with the model**. After the code has run, what does the model say, what does the screen show, and are the two the same. The cause is not the measure; the outcome is.

#### What a View Controller Does

In an imperative framework the whole screen is tied to one **view controller** object. The controller does three jobs. It reads the model and writes it into the views. It carries an event from the user, for example a button press, to the model. It also receives the calls that announce when the screen opens and closes. There is no other bridge between the model and the view: if the controller did not write a value to the screen, the screen does not know it.

The fragment below is the skeleton of an inbox screen. Because it imports the platform framework, it is not run in this lesson.

```swift
import UIKit

final class InboxController: UIViewController, FolderPickerDelegate {
    let store: MailStore
    private let titleLabel = UILabel()
    private let unreadLabel = UILabel()

    init(store: MailStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
        unreadLabel.text = store.values["unread"] ?? ""
    }

    func markAllRead() {
        store.update("unread", "0")
    }

    func folderPicker(_ picker: FolderPicker, didChoose folder: String) {
        titleLabel.text = folder
    }
}
```

The three methods show three separate paths. `viewDidLoad` writes the model to the screen. `markAllRead` only changes the model and does not touch the screen. `folderPicker(_:didChoose:)` writes to the screen but does not change the model; the folder picker updates the model itself. At first glance all three look reasonable. We will find out which of them leaves the screen equal to the model by measuring, not by reading.

#### Owning the View

Every view controller has a `view` property, and this property is loaded lazily: the view is created the first time it is requested. A controller that wants to build its view in code does so in `loadView`; in a controller that takes its view from a file, the framework does this work itself. In both cases the first method called after the view has loaded is `viewDidLoad`, and it is the usual place for adding subviews.

```swift
import UIKit

final class ProfileController: UIViewController {
    private let nameLabel = UILabel()

    override func loadView() {
        view = UIView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.frame = CGRect(x: 16, y: 80, width: 240, height: 24)
        view.addSubview(nameLabel)
    }
}
```

In this fragment `loadView` only assigns the root view; adding and positioning the label happen in `viewDidLoad`. `super.loadView()` is not called inside `loadView`, because the method that assigns the root view is `loadView` itself. A call that touches the `view` property while the view has not yet loaded starts the loading; in code written in the wrong order, this leads to surprising results.

#### Common Mistakes

The most common mistakes in an imperative framework arise from a write-to-screen call being left out somewhere, and most of them run without an error:

- **Changing the model without writing the screen.** When the button is pressed the model is updated and the label stays the same. The screen stays stale and no error appears.
- **Writing a new field in only one place.** A field is added to the model and `viewDidLoad` writes it to the screen, but the place that changes the field does not refresh the screen. A screen that looks right at first launch falls behind on later changes.
- **Forgetting the clearing call.** When a cell is reused or the folder changes, the old content is not cleared; the screen is left holding a value that the model no longer carries.
- **Not connecting the delegate.** If the weakly held delegate is `nil`, the call is skipped silently.
- **Holding the delegate strongly.** The controller and the helper object hold each other strongly, and neither is ever released.

Every item on this list has the same shape: the code runs, raises no error, and the screen stays wrong. That is why the course measures the outcome rather than the call itself, that is, whether the screen agrees with the model.

#### The Measure: Four Outcomes

The measurement compares every field a screen shows with the model's current value and returns one of four outcomes.

- **matching:** the model and the screen carry the same value.
- **stale:** both carry a value but the values differ; the screen has stayed old.
- **missing:** the model carries a value and the screen shows nothing.
- **leftover:** the screen shows a value and the model does not carry that field.

A field that is absent on both sides is written `out-of-scope` and enters no count. The core is a separate file, and it is used unchanged in every lesson of the course.

```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)")
    }
}
```

`Screen` is the record of the fields a controller shows on screen; `set` writes a field and `clear` removes it. `verdict` is the rule itself, and it does not look at the cause: it looks only at two values. `check` inspects the given keys after an event and prints each line, and `countVerdicts` counts the four outcomes.

#### Five Events

The model is a plain Swift counterpart of the controller and the objects around it. The code below does not run the framework; it is a copy that carries the type and method names of the fragment above, but whose calls are made by `main.swift`. In other words, the model builds what the code writes to the screen; it is not the framework itself. The folder picker notifies the controller through a **delegate** protocol, and the delegate reference is held **weakly**, because if the controller holds the picker strongly and the picker holds the controller strongly, the two objects cannot release each other: this is a **retain cycle**.

The five events run in order: the screen loads, all messages are marked read, the folder changes, two new fields arrive in the model, and a cell is reused with new content.

```swift
// main.swift — five events, nine fields, four agreements
final class MailStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

protocol FolderPickerDelegate: AnyObject {
    func folderPicker(_ picker: FolderPicker, didChoose folder: String)
}

final class FolderPicker {
    weak var delegate: FolderPickerDelegate?
    let store: MailStore

    init(store: MailStore) { self.store = store }

    func choose(_ folder: String) {
        store.update("title", folder)
        delegate?.folderPicker(self, didChoose: folder)
    }
}

final class InboxController: FolderPickerDelegate {
    let store: MailStore
    let screen = Screen("InboxController")

    init(store: MailStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
        screen.set("unread", store.values["unread"] ?? "")
    }

    func markAllRead() {
        store.update("unread", "0")
    }

    func folderPicker(_ picker: FolderPicker, didChoose folder: String) {
        screen.set("title", folder)
    }
}

final class MessageCell {
    let screen = Screen("MessageCell")

    func configure(_ message: [String: String]) {
        for key in message.keys.sorted() {
            screen.set(key, message[key] ?? "")
        }
    }
}

let store = MailStore()
store.update("title", "Inbox")
store.update("unread", "3")
let inbox = InboxController(store: store)
inbox.viewDidLoad()
let loaded = check("loaded", model: store.values, screen: inbox.screen, keys: ["title", "unread"])

inbox.markAllRead()
let marked = check("marked-read", model: store.values, screen: inbox.screen, keys: ["unread"])

let picker = FolderPicker(store: store)
picker.choose("Archive")
let moved = check("folder-changed", model: store.values, screen: inbox.screen, keys: ["title"])

store.update("drafts", "2")
store.update("flagged", "1")
let drafted = check("drafts-arrived", model: store.values, screen: inbox.screen, keys: ["drafts", "flagged"])

let cell = MessageCell()
cell.configure(["subject": "Invoice", "badge": "new", "attachment": "pdf"])
let next = ["subject": "Meeting"]
cell.configure(next)
let reused = check("cell-reused", model: next, screen: cell.screen, keys: ["subject", "badge", "attachment"])

countVerdicts(loaded + marked + moved + drafted + reused)
```

```
loaded | InboxController.title: model=Inbox screen=Inbox -> matching
loaded | InboxController.unread: model=3 screen=3 -> matching
marked-read | InboxController.unread: model=0 screen=3 -> stale
folder-changed | InboxController.title: model=Archive screen=Inbox -> stale
drafts-arrived | InboxController.drafts: model=2 screen=- -> missing
drafts-arrived | InboxController.flagged: model=1 screen=- -> missing
cell-reused | MessageCell.subject: model=Meeting screen=Meeting -> matching
cell-reused | MessageCell.badge: model=- screen=new -> leftover
cell-reused | MessageCell.attachment: model=- screen=pdf -> leftover
matching: 3
stale: 2
missing: 2
leftover: 2
```

The measurement checked nine fields: 3 matching, 2 stale, 2 missing, 2 leftover. The number of events (five) and the number of fields (nine) are not the same thing; the event in which the cell is reused checks three fields on its own.

Reading the lines in order: `loaded` left both fields matching, because `viewDidLoad` writes both. `marked-read` is stale: the **target-action** link changed the model but did not update the label; the model says 0 and the screen still says 3. `folder-changed` is also stale, and its cause is more hidden: because the delegate was never connected, `weak var delegate` stayed `nil`, and the `delegate?.folderPicker` call silently did nothing. The model became Archive and the screen stayed Inbox. `drafts-arrived` is two missing: the screen never wrote the `drafts` and `flagged` fields that arrived in the model. `cell-reused` is one matching and two leftover: the cell wrote the subject of the new message but did not clear the badge and attachment left from the previous message.

All four outcomes come from the same root: writing to the screen is a call, and if there is no call there is no write. Stale appears where the call was never made or never reached its target; missing, where the call was never written; leftover, where the clearing call was never written. This distribution is not itself a finding. We chose the five events; this measurement does not say how often each of these outcomes will occur in a framework.

**VC1.** In this lesson's model, the calls that write to the screen and the calls that write to the model are kept separate, and the model is always taken to be correct. Whether a real label later draws the value the controller wrote differently was not measured.

#### Reading the Measurement

The numbers above are not a checklist; they are the result of those five events. Which outcome appears on a screen, and how often, depends on the choice of events. What the measurement shows is that all four outcomes can sit side by side in a single realistic controller; it does not say which is more frequent. Stale and missing appear where the screen was not written; leftover, where it was not cleared. Matching lines are the only evidence that the screen is right, and that is why they must be counted too.

The question to ask when reading a line's outcome should be "what remained on the screen", not "what did the code do". It is possible for the screen to stay wrong where the code looks right; it is also possible for the code to look wrong where the screen is right. The measurement looks only at what remains on the screen.

#### The Bounding Measurement

The rule compares values character by character. This shows two limits together: formatted text is not counted as equal, and a field absent on both sides is not counted.

```swift
let label = "3 messages"
let screen = Screen("InboxController")
screen.set("unread", label)
let result = check("formatted-text", model: ["unread": "3"], screen: screen, keys: ["unread"])
countVerdicts(result)
screen.clear("unread")
let cleared = check("cleared", model: [:], screen: screen, keys: ["unread"])
countVerdicts(cleared)
```

```
formatted-text | InboxController.unread: model=3 screen=3 messages -> stale
matching: 0
stale: 1
missing: 0
leftover: 0
cleared | InboxController.unread: model=- screen=- -> out-of-scope
matching: 0
stale: 0
missing: 0
leftover: 0
```

The model says `3` and the screen shows `3 messages`. To a human eye the two agree, but the rule counts this as stale, because the rule does not look at the cause and does not know about formatting. When the screen is emptied with `clear` and the model has no such field either, the line prints `out-of-scope` and all four counts stay zero. Because the field is on neither the model nor the screen, there is nothing to compare.

These two limits hold in later lessons as well. A lesson that measures a formatted label either writes the formatted text to the model too, or does the comparison on the unformatted value and says so explicitly.

#### Summary

- The view controller is the object that carries the model to the screen and the events on the screen to the model; the screen does not know a value that was not written to it.
- The measure is "does this screen agree with the model" and gives four outcomes: matching, stale, missing, leftover.
- Across five events, nine fields were checked: 3 matching, 2 stale, 2 missing, 2 leftover.
- An unconnected weak delegate raises no error; the call is silently not made and the screen stays stale.
- The rule does not count formatted text as equal and does not count a field absent on both sides.
- This lesson's agreement: **all four** (3/2/2/2).

#### Next Step

In this lesson the call that wrote the screen was `viewDidLoad`, and we read it as a setup that runs once. In reality a controller is loaded, appears, disappears and appears again, and each stage has its own call. In the next lesson we will measure how the place where we write a value among the lifecycle methods can separate the screen from the model.

### Lifecycle Methods

Source: https://academia.sh/en/courses/uikit/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.

### Navigation Controllers

Source: https://academia.sh/en/courses/uikit/navigation-controllers

Measuring that when a screen is pushed and popped in a navigation controller, a selection removed from the model stays on the list as leftover, and that the pushed screen matches the model.

In the previous lesson the screen came onto the screen once, stayed beneath another screen, and came back. Where did that other screen come from? In most apps, screens are placed on top of one another with a **navigation controller**. The navigation controller holds the screens in a **stack**: a new screen is **pushed** onto the top of the stack, and when the user goes back, the top screen is **popped** off the stack and the screen beneath it becomes visible again.

This is a management job; the navigation controller does not manage what the screens contain. What the screen left underneath shows is whatever that screen's own controller wrote. In this lesson we measure a consequence of that: the list screen has a selected row, the user goes down into the detail and comes back; the selection is gone from the model, but does the list know about it.

#### Push and Pop

The fragment below shows a contact list and a detail screen. Because it imports the platform framework, it is not run.

```swift
import UIKit

final class ContactListController: UIViewController {
    let store: ContactStore
    private let selectedLabel = UILabel()
    private let countLabel = UILabel()

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        countLabel.text = store.values["count"] ?? ""
    }

    func select(_ name: String) {
        store.update("selected", name)
        selectedLabel.text = name
        navigationController?.pushViewController(ContactDetailController(store: store), animated: true)
    }
}

final class ContactDetailController: UIViewController {
    let store: ContactStore
    private let selectedLabel = UILabel()
    private let phoneLabel = UILabel()

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        selectedLabel.text = store.values["selected"] ?? ""
        phoneLabel.text = store.values["phone"] ?? ""
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if parent == nil { store.remove("selected") }
    }
}
```

`select` writes the selection to both the model and the list's label, and pushes the detail. When the detail controller is removed from the stack, `didMove(toParent:)` receives the parent as `nil` and removes the selection from the model. There is no code in the list's own controller that clears the selection label.

#### The Controller That Manages the Stack

A navigation controller keeps a stack made of child controllers. The topmost controller is visible on the screen; `pushViewController` puts a controller on top of the stack, and `popViewController` removes the topmost one. The stack holds the controllers inside it strongly; so a controller waiting in the stack stays in memory and its view is not loaded again.

```swift
import UIKit

final class ContactListController: UIViewController {
    let store: ContactStore

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func showDetail(for name: String) {
        let detail = ContactDetailController(store: store, name: name)
        navigationController?.pushViewController(detail, animated: true)
    }
}
```

The `navigationController` property gives the navigation controller that the controller sits in; if the controller is not in a stack it returns `nil`, and the push call silently does nothing. The back button in the navigation bar is connected by the framework itself: a tap pops the topmost controller, and there is no need to write the `popViewController` call in code.

#### The Measure

The measure is the same core throughout the course.

```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)")
    }
}
```

#### The List After Returning

The model builds the two controllers in the skeleton and the stack in plain Swift. The model builds what the code writes to the screen; it is not the framework itself. The navigation controller keeps only a list of names, because what is measured is not the stack itself but what the screens write when the stack changes. The order of the pop and the `didMove(toParent:)` call is the lesson's choice.

The events are, in order: the list loads and a contact is selected, the detail is pushed and loads, the detail is popped.

```swift
// main.swift — list, detail, pop
final class ContactStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class ContactNavigation {
    private(set) var stack: [String] = []
    func push(_ name: String) { stack.append(name) }
    func pop() { _ = stack.popLast() }
}

final class ContactListController {
    let store: ContactStore
    let screen = Screen("ContactListController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("count", store.values["count"] ?? "")
    }

    func select(_ name: String) {
        store.update("selected", name)
        screen.set("selected", name)
    }
}

final class ContactDetailController {
    let store: ContactStore
    let screen = Screen("ContactDetailController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("selected", store.values["selected"] ?? "")
        screen.set("phone", store.values["phone"] ?? "")
    }

    func didMove(toParent parent: ContactNavigation?) {
        if parent == nil { store.remove("selected") }
    }
}

let store = ContactStore()
store.update("count", "2")
store.update("phone", "555-0100")
let navigation = ContactNavigation()
let list = ContactListController(store: store)
navigation.push("ContactListController")
list.viewDidLoad()
list.select("Alice")
let chosen = check("selected", model: store.values, screen: list.screen, keys: ["selected", "count"])

let detail = ContactDetailController(store: store)
navigation.push("ContactDetailController")
detail.viewDidLoad()
let pushed = check("detail-pushed", model: store.values, screen: detail.screen, keys: ["selected", "phone"])

navigation.pop()
detail.didMove(toParent: nil)
let popped = check("popped", model: store.values, screen: list.screen, keys: ["selected", "count"])

countVerdicts(chosen + pushed + popped)
```

```
selected | ContactListController.selected: model=Alice screen=Alice -> matching
selected | ContactListController.count: model=2 screen=2 -> matching
detail-pushed | ContactDetailController.selected: model=Alice screen=Alice -> matching
detail-pushed | ContactDetailController.phone: model=555-0100 screen=555-0100 -> matching
popped | ContactListController.selected: model=- screen=Alice -> leftover
popped | ContactListController.count: model=2 screen=2 -> matching
matching: 5
stale: 0
missing: 0
leftover: 1
```

The selection and the detail are matching in the first two events. In the third event the detail controller updated the model correctly: the selection is gone from the model. But the list's label still says Alice. The line came out `leftover`: the screen shows a value, and the model does not carry that field. The `count` field never changed, so it stayed matching in the same list. The problem is not that the detail controller wrote wrongly; it is that the list wrote nothing. Changing the model does not change the screen.

This result occurs even though the list becomes visible again when the detail screen leaves the stack. When the list reappears, the `viewWillAppear` we saw in the previous lesson is called, but the list has no code that overrides that method and brings the selection level with the model. The fix is to rewrite the selection label from the model in that method, or to clear it if the model is empty. This lesson's measurement does not measure the fix; it only shows the problem.

**VC1.** In the model, the call that removes the selection is inside `didMove(toParent:)`, after the pop. The order of these two events in the real framework was not measured; the result does not depend on the order, because the list writes in neither of these calls.

#### Common Mistakes

- **Assuming the list is reloaded.** When we return from the detail, the list controller was waiting in its place in the stack; `viewDidLoad` is not called again. If the value was written only there, the screen stays old.
- **Catching the pop with `viewWillDisappear`.** This method is also called when a new controller is pushed on top. To tell that a controller has really left the stack, `isMovingFromParent` or `didMove(toParent:)` is used.
- **Pushing the same controller instance twice.** The framework does not support this and gives an error at run time. A new instance is created for each push.
- **Pushing from a controller that is not in a stack.** If `navigationController` is `nil`, the push is silently not made.
- **Forgetting to clear in the list the state the detail left behind.** The detail screen marks the selected contact, and if the mark is not erased when returning to the list, a leftover value stays in the list. This is the result this lesson measures.

#### Reading the Measurement

Of the six lines, five are matching and one is leftover. The leftover line is the `selected` field that stays on the list screen after the pop: the model no longer carries that field, and the screen still shows it. This is not a missing write but a missing deletion; because the list controller kept living in the stack, no one was assigned the clearing call.

The striking side is that this mistake does not come from the detail screen. The detail controller did its own work and removed the value from the model inside `didMove(toParent:)`. The screen agreeing with the model depends on the own write call of every screen on which the model changes; when the model changes, the other controllers in the stack do not hear about it. A list screen waiting in the stack can read the changed model only when it appears again, and if no one wrote the read, it does not read.

#### Title, Bar Buttons and Rebuilding the Stack

Each controller defines what will appear in the navigation bar through `navigationItem`: `title` gives the bar's title and `rightBarButtonItem` gives the button on the right. The title of the back button comes from the previous controller's title. These settings are made inside `viewDidLoad`; as long as the controller stays in the stack, writing them once is enough.

```swift
import UIKit

final class ContactDetailController: UIViewController {
    let store: ContactStore
    let name: String

    init(store: ContactStore, name: String) {
        self.store = store
        self.name = name
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = name
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if parent == nil {
            store.remove("selected")
        }
    }
}
```

The stack can do more than a single push and pop. `popToRootViewController` removes everything down to the root controller; the `viewControllers` array gives the whole stack; `setViewControllers` rebuilds the stack entirely. Each of these means that the controllers that leave receive lifecycle calls. After a call that removes several controllers at once, it is necessary to make sure that the state left by all of them has been cleared.

For two screens to show the same data, a shared model object is often given: the list and the detail look at the same store. This guarantees that the screens will read from the same source, but it does not guarantee when the read call will be made. The lesson's measurement shows this gap.

There is one more detail: the `pushViewController` call adds to the stack, but does not wait for the transition to finish visually. Code written right after the call runs at a moment when the new controller has not yet appeared. For this reason, work that depends on the new screen's content is left to the new controller's own lifecycle methods.

#### The Bounding Measurement

This measurement has two limits. The first: the pushed detail is a new `Screen` and has not written anything yet. If the core checks this screen before `viewDidLoad`, it says `missing`, because the model carries a value. This is not a deviation; it is the screen not having been set up yet. The second: a popped screen leaves the stack and is no longer checked; the measurement looks only at visible screens.

```swift
// main.swift — the pushed screen and the popped screen
final class ContactStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class ContactNavigation {
    private(set) var stack: [String] = []
    func push(_ name: String) { stack.append(name) }
    func pop() { _ = stack.popLast() }
}

final class ContactDetailController {
    let store: ContactStore
    let screen = Screen("ContactDetailController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("selected", store.values["selected"] ?? "")
        screen.set("phone", store.values["phone"] ?? "")
    }
}

let store = ContactStore()
store.update("selected", "Alice")
store.update("phone", "555-0100")
let navigation = ContactNavigation()
navigation.push("ContactListController")
let detail = ContactDetailController(store: store)
navigation.push("ContactDetailController")
let before = check("pushed-before-writing", model: store.values, screen: detail.screen, keys: ["selected", "phone"])
detail.viewDidLoad()
let after = check("pushed-after-writing", model: store.values, screen: detail.screen, keys: ["selected", "phone"])
navigation.pop()
print(navigation.stack.joined(separator: ", "))
countVerdicts(before + after)
```

```
pushed-before-writing | ContactDetailController.selected: model=Alice screen=- -> missing
pushed-before-writing | ContactDetailController.phone: model=555-0100 screen=- -> missing
pushed-after-writing | ContactDetailController.selected: model=Alice screen=Alice -> matching
pushed-after-writing | ContactDetailController.phone: model=555-0100 screen=555-0100 -> matching
ContactListController
matching: 2
stale: 0
missing: 2
leftover: 0
```

The rule does not know when a screen counts as set up. The pushed screen comes out `missing` before it writes and `matching` after it writes; for this reason the lesson checks the screen after the setup call. At the end of the pop, only the list remains in the stack. The popped detail screen is not checked again, because it is no longer a visible screen.

#### Summary

- A navigation controller holds screens in a stack; push adds to the stack, pop removes the top one.
- The screen left underneath does not update by itself when the screen above changes the model.
- A selection removed from the model is leftover if it is still visible in the list's label.
- The pushed screen writes matching to the model during the setup call; checking it makes sense after this call.
- The popped screen leaves the stack and stays outside the measurement.
- This lesson's agreement: **leftover**.

#### Next Step

The navigation controller adds the new screen to the stack. Not every screen is added to the stack: some open over the current screen and leave nothing in its place when they close. In the next lesson we will cover presentation styles and measure whether a value entered in a note window reaches the screen left underneath.

### Presentation Styles

Source: https://academia.sh/en/courses/uikit/presentation-styles

Measuring that a value entered on a note screen presented modally over another screen is written to the model but never reaches the screen beneath, and stays missing there.

In the previous lesson screens were placed on top of one another in a stack. There is one more way to show a screen: opening it over the current screen as a **modal presentation**. A modal presentation does not add to the stack; the presented screen sits above the presenting screen and leaves its place empty when it is dismissed. Adding to the stack is called a **push presentation**.

This distinction matters for the measurement, because in a modal presentation there is no ready-made link between the two screens. A value entered on the presented screen does not reach the presenting screen by itself. In this lesson we measure a case where the entry is written to the model but the presenting screen never shows it.

#### Presenting and Dismissing

The fragment below shows a task board and a note screen opened over it. Because it imports the platform framework, it is not run.

```swift
import UIKit

final class TaskBoardController: UIViewController {
    let store: TaskStore
    private let titleLabel = UILabel()
    private let openLabel = UILabel()

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
        openLabel.text = store.values["open"] ?? ""
    }

    func addNote() {
        let controller = NoteController(store: store)
        controller.modalPresentationStyle = .pageSheet
        present(controller, animated: true)
    }
}

final class NoteController: UIViewController {
    let store: TaskStore
    private let titleLabel = UILabel()
    private let noteLabel = UILabel()

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
    }

    func save(_ text: String) {
        store.update("note", text)
        noteLabel.text = text
        dismiss(animated: true)
    }
}
```

`addNote` chooses the presentation style and opens the note screen on top; `save` writes to both the model and its own label and dismisses itself. There is no line in the board's controller that reads the `note` key. The presentation style, whether it is a page or full screen, can also affect whether the presenting screen's reappearance methods are called after the dismissal. This lesson does not measure that and does not rely on it.

#### Presentation Styles

Besides adding to a navigation stack, the second way is to **present** one controller over another. A modal presentation opens the new screen over the current one and cuts off interaction with the screen beneath until the user dismisses it. The style is chosen with the `modalPresentationStyle` property: `fullScreen` covers the screen entirely, and `pageSheet` shows a sheet that leaves the screen beneath partly uncovered.

```swift
import UIKit

final class TaskBoardController: UIViewController {
    let store: TaskStore

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func addNote() {
        let note = NoteController(store: store)
        note.modalPresentationStyle = .pageSheet
        present(note, animated: true)
    }
}

final class NoteController: UIViewController {
    let store: TaskStore

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func saveTapped() {
        store.update("note", "Call back")
        dismiss(animated: true)
    }
}
```

The presenting controller calls `present`, and the presented controller dismisses itself with `dismiss`. When the presented screen is dismissed, the presenting screen merely becomes visible again; what the presented controller produced is not carried to the presenter by itself.

#### The Measure

The core is the same as in the earlier lessons.

```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)")
    }
}
```

#### The Board When a Note Is Saved

The model builds the two controllers in plain Swift. The model builds what the code writes to the screen; it is not the framework itself. `present` and `dismiss` only record which screen is on top. The events are: the board loads, the note screen is presented and loads, the user saves a note, and the note screen is dismissed.

```swift
// main.swift — board, modal presentation, note
final class TaskStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class TaskBoardController {
    let store: TaskStore
    let screen = Screen("TaskBoardController")
    private(set) var presented: String?

    init(store: TaskStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
        screen.set("open", store.values["open"] ?? "")
    }

    func present(_ name: String) { presented = name }
    func dismiss() { presented = nil }
}

final class NoteController {
    let store: TaskStore
    let screen = Screen("NoteController")

    init(store: TaskStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
    }

    func save(_ text: String) {
        store.update("note", text)
        screen.set("note", text)
    }
}

let store = TaskStore()
store.update("title", "Report")
store.update("open", "4")
let board = TaskBoardController(store: store)
board.viewDidLoad()
let loaded = check("loaded", model: store.values, screen: board.screen, keys: ["title", "open"])

let note = NoteController(store: store)
board.present("NoteController")
note.viewDidLoad()
let presented = check("note-presented", model: store.values, screen: note.screen, keys: ["title"])

note.save("Draft")
board.dismiss()
let saved = check("note-saved", model: store.values, screen: note.screen, keys: ["note"])
let closed = check("note-closed", model: store.values, screen: board.screen, keys: ["title", "open", "note"])

countVerdicts(loaded + presented + saved + closed)
```

```
loaded | TaskBoardController.title: model=Report screen=Report -> matching
loaded | TaskBoardController.open: model=4 screen=4 -> matching
note-presented | NoteController.title: model=Report screen=Report -> matching
note-saved | NoteController.note: model=Draft screen=Draft -> matching
note-closed | TaskBoardController.title: model=Report screen=Report -> matching
note-closed | TaskBoardController.open: model=4 screen=4 -> matching
note-closed | TaskBoardController.note: model=Draft screen=- -> missing
matching: 6
stale: 0
missing: 1
leftover: 0
```

The note screen is matching in its own field: the user sees what they entered on their screen, and the model carries the same value. The board stayed matching in `title` and `open`, because no one changed them during the presentation. The `note` field is new in the model and the board's screen never wrote it; the line came out `missing`. When the presentation closes, the board is not updated by itself, and there is no code that knows who holds which piece of information.

A modal presentation leaves the board running but does not tell it anything. For the news to arrive, the presenting screen has to step in; the known way is a delegate protocol. That protocol was built for the folder picker in the opening lesson and can be used the same way here. What this lesson measures is the gap itself.

**VC1.** In the model, `save` writes the presented screen's own label and `dismiss` only removes the screen on top. Which lifecycle methods the presenting screen receives during the dismissal, depending on the style, was not measured in the real framework; it is assumed that the board writes `note` nowhere, so the result does not depend on the calls.

#### Common Mistakes

- **Assuming the result reaches the presenter.** The presented screen changes the model, but if there is no write call when the presenting screen becomes visible again, it shows the old value. This is the missing result this lesson measures.
- **Expecting the presented screen to hide the presenter.** In partial styles such as `pageSheet`, the screen beneath is not counted as gone from the lifecycle; `viewWillAppear` may not be called on return. In the full-screen style it is called. Code that ties the update to these methods stops working silently when the style changes.
- **Trusting the dismiss button.** In the page style, the user can also dismiss the screen by pulling it down. If a save call lives only on the button, this path skips it; the dismissal can be blocked with `isModalInPresentation`, or the exit can be handled with a delegate.
- **Starting a second presentation while one is in progress.** The framework rejects it and no presentation is made.
- **Making the callback link strong while the presenter holds the presented controller.** If the delegate is not held weakly, the two controllers never release each other.

#### Reading the Measurement

Of the seven lines, six are matching and one is missing. The missing line is the `note` field that does not appear on the board after the note is saved and the screen is dismissed: the model carries the note, and the board screen never wrote it. The save happened; what was lost is carrying the result to the board.

The matching lines in this result have a meaning: the note screen's own fields stay in agreement with the model for as long as it is presented. The error appeared on the boundary between the presenting and the presented screen. A modal presentation gives the two screens separate lifecycles and leaves the carrying between them to the programmer. The fix is to have both screens read from the model, or to have the presenter rewrite its fields when the presented screen is dismissed; which one to choose depends on the structure of the screen.

#### Carrying the Result to the Presenter

The usual way to deliver the presented screen's result to the presenter is a delegate protocol. The presented controller calls the protocol, and the presenting controller implements it. The delegate reference is held weakly, because the presenting controller already holds the presented one and a strong link in the opposite direction would create a retain cycle.

```swift
import UIKit

protocol NoteControllerDelegate: AnyObject {
    func noteController(_ controller: NoteController, didSave text: String)
}

final class NoteController: UIViewController {
    weak var delegate: NoteControllerDelegate?

    func saveTapped() {
        delegate?.noteController(self, didSave: "Call back")
        dismiss(animated: true)
    }
}

extension TaskBoardController: NoteControllerDelegate {
    func noteController(_ controller: NoteController, didSave text: String) {
        store.update("note", text)
        // write the board field here as well
    }
}
```

The delegate call brings two jobs together: it hands the result to the presenter and opens a place for the presenter to write its screen. If the caller did not assign a delegate, the call is silently skipped; this is the same unconnected delegate case we saw in the first lesson.

#### Presentation Style and Ways to Dismiss

In the full-screen style the presenting screen leaves the view and becomes visible again when the presented one is dismissed; in the page style the screen beneath keeps showing behind it. This difference changes when the lifecycle calls will run on the presenter. Tying the update of the screen beneath to the return call breaks when the style changes; tying the update to the delegate call does not depend on the style.

When the user dismisses the sheet by pulling it down, the button's action does not run. If the save call lives only on the button, this path skips the save. Setting `isModalInPresentation = true` prevents the pull-down and steers the user to the explicit exits, the buttons. An alert is shown with the same presentation call; when the alert is dismissed, the presenter's screen still depends on its own write call.

When data has to be given to the presented controller, it is assigned before the `present` call. Assigning after the call can make the presented screen open with empty data on its first display.

#### The Bounding Measurement

The measurement looks only at the visible screen. The object of the dismissed note screen may stay in memory and carry an old value, but because that screen is no longer shown, it is not checked. This means the screen is outside the measurement; it is not an `out-of-scope` field.

```swift
// main.swift — the dismissed screen is not checked
final class TaskStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class TaskBoardController {
    let screen = Screen("TaskBoardController")
    private(set) var presented: String?
    func present(_ name: String) { presented = name }
    func dismiss() { presented = nil }
}

final class NoteController {
    let store: TaskStore
    let screen = Screen("NoteController")

    init(store: TaskStore) { self.store = store }

    func save(_ text: String) {
        store.update("note", text)
        screen.set("note", text)
    }
}

let store = TaskStore()
let board = TaskBoardController()
let note = NoteController(store: store)
board.present("NoteController")
note.save("Draft")
board.dismiss()
store.remove("note")
print("presented: \(board.presented ?? "none")")
print("left on the dismissed screen: \(note.screen.shown["note"] ?? "-")")
let visible = check("visible-board", model: store.values, screen: board.screen, keys: ["note"])
countVerdicts(visible)
```

```
presented: none
left on the dismissed screen: Draft
visible-board | TaskBoardController.note: model=- screen=- -> out-of-scope
matching: 0
stale: 0
missing: 0
leftover: 0
```

After the note screen was dismissed, the model deleted the note. `Draft` is still in the dismissed screen's object; had the core checked that screen, the line would have come out leftover. But the presented screen is `none` and the only screen checked is the board. For this reason the measurement gives zero counts and does not report the dismissed screen's deviation. A screen leaving the measurement does not mean the screen agrees with the model.

#### Summary

- A modal presentation puts the presented screen on top of the presenting screen and does not add to the stack; when it is dismissed, the presenting screen stays as it was.
- A value entered on the presented screen can be written to the model but does not reach the presenting screen by itself.
- A field the presenting screen never writes becomes missing the moment it enters the model.
- Fields that do not change during the presentation stay matching on the presenting screen.
- The measurement looks only at the visible screen; a dismissed screen does not enter the count.
- This lesson's agreement: **missing**.

#### Next Step

So far we built screens in code: the labels were fields of the controller. Many projects draw the screen in a visual editor and connect the labels from a file. In the next lesson we will compare the visual interface editor with a code-built interface and measure a case where a label whose connection is broken silently stays unwritten.

### The Visual Editor and Code-Built Interfaces

Source: https://academia.sh/en/courses/uikit/visual-editor-and-code

Measuring that a label with a broken outlet stays stale with its placeholder text on a screen drawn in the visual editor, that a code-built label matches, and that the placeholder text the editor saves stays as leftover.

In the earlier lessons the labels were fields the controller built in code. The second way to build a screen is to draw it in the **visual interface editor**: labels, buttons and the layout between them are saved to a file and loaded from the file while the app runs. The third way is a mix of the two: some views in the file, some in code.

Which way is chosen does not change the controller: writing to the screen is still the controller's job. What changes is how the controller reaches the view. A view that comes from the file is handed to the controller through an **outlet**; a view built in code is created by the controller itself. In this lesson we measure a case where the first can break silently.

#### An Outlet and a Code-Built Label

The fragment below shows a weather screen. The temperature and humidity labels are drawn in the file and handed over through outlets; the wind label is built in code. Because it imports the platform framework, it is not run.

```swift
import UIKit

final class WeatherController: UIViewController {
    let store: WeatherStore
    @IBOutlet var temperatureLabel: UILabel?
    @IBOutlet var humidityLabel: UILabel?
    private let windLabel = UILabel()

    init(store: WeatherStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        temperatureLabel?.text = store.values["temperature"] ?? ""
        humidityLabel?.text = store.values["humidity"] ?? ""
        windLabel.text = store.values["wind"] ?? ""
    }
}
```

The outlets are written as optional values. If a label was drawn in the file but not connected to the controller, or the connection was deleted later, the field stays `nil`. The `?.` form of the chain silently skips a write to a `nil` field: no error is raised and no warning appears. Defining the field with an implicitly unwrapped type would choose to stop with an error in the same case; this lesson's measurement deals with the result of the chained form.

#### Loading from the File

When the screen is drawn in the file, the framework reads the file, creates the views and assigns the outlets to the controller. The assignment finishes before `viewDidLoad` is called; so the outlets are expected to be filled inside `viewDidLoad`. If they are not filled, the field stays `nil`.

The same screen can also be built in code. In that case the view and the labels are created by the controller itself, and they have no chance of being `nil`:

```swift
import UIKit

final class WeatherCodeController: UIViewController {
    private let temperatureLabel = UILabel()

    override func loadView() {
        view = UIView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        temperatureLabel.frame = CGRect(x: 16, y: 80, width: 200, height: 24)
        view.addSubview(temperatureLabel)
    }
}
```

A mix of the two is also common: the main layout in the file, the dynamic parts in code. Which way is chosen makes no difference to the measurement; the difference is how fragile the path to the write call is.

#### The Measure

The core is the same as in the earlier lessons.

```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)")
    }
}
```

#### With the Outlet Broken and Connected

The model builds the controller from the fragment in plain Swift. The model builds what the code writes to the screen; it is not the framework itself. A label is a small type that carries a name and a screen; the outlets are assigned in `main.swift`, because that is the job of the editor that loads the file. The two labels from the file are loaded carrying the placeholder text (`Label`) they were drawn with in the editor; `loadInterfaceFile` sets this up. The temperature label is not connected at first. Then the outlet is connected and the screen is loaded again.

```swift
// main.swift — broken outlet, code-built label
final class WeatherStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class ConnectedLabel {
    let key: String
    let screen: Screen

    init(_ key: String, screen: Screen) {
        self.key = key
        self.screen = screen
    }

    func setText(_ text: String) { screen.set(key, text) }
}

final class WeatherController {
    let store: WeatherStore
    let screen = Screen("WeatherController")
    var temperatureLabel: ConnectedLabel?
    var humidityLabel: ConnectedLabel?
    let windLabel: ConnectedLabel

    init(store: WeatherStore) {
        self.store = store
        windLabel = ConnectedLabel("wind", screen: screen)
    }

    func loadInterfaceFile() {
        screen.set("temperature", "Label")
        screen.set("humidity", "Label")
    }

    func viewDidLoad() {
        temperatureLabel?.setText(store.values["temperature"] ?? "")
        humidityLabel?.setText(store.values["humidity"] ?? "")
        windLabel.setText(store.values["wind"] ?? "")
    }
}

let store = WeatherStore()
store.update("temperature", "21")
store.update("humidity", "60")
store.update("wind", "12")
let controller = WeatherController(store: store)
controller.humidityLabel = ConnectedLabel("humidity", screen: controller.screen)
controller.loadInterfaceFile()
controller.viewDidLoad()
let broken = check("loaded", model: store.values, screen: controller.screen, keys: ["temperature", "humidity", "wind"])

controller.temperatureLabel = ConnectedLabel("temperature", screen: controller.screen)
controller.loadInterfaceFile()
controller.viewDidLoad()
let connected = check("connected", model: store.values, screen: controller.screen, keys: ["temperature", "humidity", "wind"])

countVerdicts(broken + connected)
```

```
loaded | WeatherController.temperature: model=21 screen=Label -> stale
loaded | WeatherController.humidity: model=60 screen=60 -> matching
loaded | WeatherController.wind: model=12 screen=12 -> matching
connected | WeatherController.temperature: model=21 screen=21 -> matching
connected | WeatherController.humidity: model=60 screen=60 -> matching
connected | WeatherController.wind: model=12 screen=12 -> matching
matching: 5
stale: 1
missing: 0
leftover: 0
```

On the first load the temperature came out `stale`: the model says 21 and the screen keeps showing the `Label` text that came from the file. The call `temperatureLabel?.setText(...)` was skipped because the field was `nil`; the label stays on the screen, but the code cannot reach it. Humidity is connected and wind is built in code, so both match. Once the outlet is connected and the screen is loaded again, all three match.

The point to notice is how invisible this error is. A broken outlet gives no compile error, no run-time error and leaves no trace in test output; only the placeholder text from the editor shows on the screen. A code-built label is not affected by this path, because the controller creates it itself and it cannot be `nil`. This does not mean one method is better than the other: building in code removes the outlet error, but it also gives up the speed of seeing the layout in the editor.

**VC1.** In the model the outlets are assigned in `main.swift`. In the real framework the framework assigns the outlets while loading the file, and that assignment is assumed to finish before `viewDidLoad`; the lesson does not measure that order.

#### Common Mistakes

- **Leaving the outlet broken in the file.** The label is drawn but not connected to the controller. A chained write silently writes nothing to a `nil` field, and the editor's placeholder text stays on the label.
- **Renaming the outlet in code without updating the file.** The file still looks for the old name; a run-time error occurs during loading. This error is the opposite of a broken outlet: it is noisy and noticed early.
- **Deleting the outlet in code and leaving it in the file.** The same kind of mismatch, the same result.
- **Leaving the placeholder text.** The sample text the editor writes into the label stays on the screen until code writes a value. This is the lesson's bounding measurement.
- **Taking weak or strong outlets for a rule.** Because the view is already held in the root view's tree, the outlet can be defined either way; in this lesson both work. The choice is not an error. Code that removes a view from the tree and adds it back later needs a strong outlet, because a weak outlet does not hold a view that has left the tree.

One protection for fields that come from the file is to handle the `nil` case separately instead of skipping the write call silently with an optional chain; this course's measurement shows that the error is invisible, it does not make it visible.

#### Reading the Measurement

Of the six lines, five are matching and one is stale; the stale line is the temperature label with the broken outlet. When the outlet is connected and the screen is loaded again, the line becomes matching. So the field exists in the model, the code tries to write it, and the screen still shows the placeholder from the file: a skipped write cannot be told apart from a write that was never made.

This brings a difference compared to the code-built label, but it does not mean an advantage. The error of a code-built label shows up elsewhere; if it was not added to the view, for example, it does not show on the screen and gives the missing result. The measurement counts only the result on the screen.

#### What Is Saved in the File

The file the editor saves holds more than the positions of the views. It contains the view tree, the settings of each view, the constraints, the sample text of the labels, the controller's class name, and the records of the outlets and actions. The outlets and actions are stored in the file by property and method names; if the code changes one of these names, the file keeps looking for the old name.

It is also possible to load the file under a specific name:

```swift
import UIKit

final class WeatherController: UIViewController {
    @IBOutlet weak var temperatureLabel: UILabel?

    init() {
        super.init(nibName: "WeatherController", bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
}
```

If the controller's class name is set to another class in the file, the outlets saved in the file are assigned during loading to names that class does not have, and the app crashes at run time with a key error. This symptom is not silent. If the file has no outlets at all there is no crash; then only your own class's code does not run.

#### Which Way to Choose

The choice depends on the team and the screen, and is not measured in this lesson. Still, the limits of code and of the file should be known:

- The file lets you see the layout as a drawing; layout changes come as a text difference in the file, and merging is hard when two people change the same screen.
- Code makes it easy to review every change line by line; to see the layout you have to run it.
- If the parts of the screen come and go depending on state, those parts are mostly built in code.

The two ways can also be used together on one screen. In that case, keeping it clear in the controller which field comes from the file and which from code shortens the search for why a field stayed `nil`.

#### Common Problems and Their Symptoms

Some of the problems that appear when a screen comes with a file stop at run time, and some are silent: a field stays `nil`, or the write call goes to an object that is not on the screen. Their causes fall under a few headings:

- The outlet exists in the file, but the property in the controller was deleted or renamed. When the file is loaded it cannot find the name and may stop with a key error at run time.
- The controller's class is not set in the file. The system creates a plain `UIViewController` and your own class's code never runs.
- The field is read too early in the lifecycle. Outlets are assigned after the view is loaded; a field read inside `init` returns `nil`.
- The same field is created both in the file and in code. One of them enters the screen, and the other stays as a separate object that is never seen, and the write calls go to it.

All four of these symptoms are sources of the divergence between the model and the screen. In the silent ones the placeholder from the file stays on the label, and the divergence shows up in the measurement as `stale`: the model carries one value, the screen shows another. The measurement does not say the cause of the error; it only shows that the screen did not reach the model.

#### Interpreting the Preview

The preview in the editor shows how the screen looks with the sample texts in the file. There is no real data at that moment: the text in the labels is a placeholder. When the screen runs, model values are written over these placeholders. If the write call never runs, the user keeps seeing the placeholder text. What this looks like in the measurement depends on the field: if the model carries that field the line is `stale` (the temperature in the first measurement), and if it does not, `leftover` (`caption` in the bounding measurement below).

For this reason placeholder texts are chosen so that they do not look like real data. If a real name is written into a label, the error of a skipped write call goes unnoticed in the preview and at first glance. Each time the screen opens, one has to look at the running model and not the preview; the difference between the two sources is also the subject of the measurement. This distinction seems small, but it is the main reason the error is carried all the way to the release in teams that approve a screen by looking only at the preview.

#### The Bounding Measurement

The editor saves the text of a label to the file. When a label is drawn, it holds a placeholder text, and when loaded from the file that text is on the screen. If the code does not write that label, the text stays on the screen, and the model has no counterpart for that field.

```swift
// main.swift — placeholder text from the file
final class WeatherStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class WeatherController {
    let store: WeatherStore
    let screen = Screen("WeatherController")

    init(store: WeatherStore) { self.store = store }

    func loadInterfaceFile() {
        screen.set("caption", "Label")
    }

    func viewDidLoad() {
        screen.set("temperature", store.values["temperature"] ?? "")
    }
}

let store = WeatherStore()
store.update("temperature", "21")
let controller = WeatherController(store: store)
controller.loadInterfaceFile()
controller.viewDidLoad()
let placeholder = check("placeholder", model: store.values, screen: controller.screen, keys: ["temperature", "caption"])
countVerdicts(placeholder)
```

```
placeholder | WeatherController.temperature: model=21 screen=21 -> matching
placeholder | WeatherController.caption: model=- screen=Label -> leftover
matching: 1
stale: 0
missing: 0
leftover: 1
```

`caption` shows `Label` on the screen and the model does not carry that field; the line is `leftover`. Nothing the code author did was wrong: the label stayed in the file and no one deleted it. The rule cannot tell an accident from an intention. The placeholder could have been a title left on purpose; the rule does not look at why.

#### Summary

- A screen can be built in the visual editor or in code; in both ways, writing to the screen is the controller's job.
- A label that comes from the file is handed over through an outlet; if the outlet is broken, the optional chain silently skips the write and the label stays stale with the placeholder text.
- A code-built label cannot be `nil` and is not affected by this path.
- When the outlet is connected and the screen is loaded again, the field becomes matching.
- The placeholder text the editor saves in the file stays on the screen as leftover if the code does not write it.
- This lesson's agreement: **stale**.

#### Next Step

The outlet is the controller's hand reaching toward a view on the screen. There is a link in the opposite direction too: pressing a button calls a method on the controller. In the next lesson we will cover outlets and actions together and measure a case where the action updates the model and forgets the label.

### Outlets and Actions

Source: https://academia.sh/en/courses/uikit/outlets-and-actions

Measuring that an action that only updates the model and forgets the label leaves the screen stale, and that formatted text compared with the raw value counts as stale.

In the previous lesson we saw the path from the controller to the view: the controller writes to a label drawn in the file through an **outlet**. There is a link in the opposite direction too. When the user presses a button, the framework calls a particular method on the controller; the link that sets up this call is called an **action**, and it is part of the **target-action** pattern: the button knows the target (the controller) and the action (the method name), and calls it on a touch.

The job of this method is usually two parts: changing the model and reflecting it on the screen. Forgetting to write one of the two is very easy, because both sit side by side in the same method.

#### When the Action Updates the Model

The fragment below shows a cart screen. The quantity label is handed over through an outlet; the button's action increases the quantity. Because it imports the platform framework, it is not run.

```swift
import UIKit

final class CartController: UIViewController {
    let store: CartStore
    @IBOutlet var quantityLabel: UILabel?

    init(store: CartStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        quantityLabel?.text = store.values["quantity"] ?? ""
    }

    @IBAction func addTapped(_ sender: Any) {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
    }
}
```

`addTapped` updates the model. It writes nothing to the label. When the screen opens, the label is read correctly; after the first touch the model says 2 and the label keeps showing 1.

#### Connecting the Action

There are two ways to connect an action to a button. For a button drawn in the file, the action is set up with a link dragged in the visual editor from the button to a method in the controller; the method is marked with `@IBAction`. For a button built in code, the target and the action are given explicitly:

```swift
import UIKit

final class CartCodeController: UIViewController {
    private let addButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
        view.addSubview(addButton)
    }

    @objc private func addTapped() {
        // update the model, then write the screen
    }
}
```

`#selector` points to the method by its name, and the method has to be marked `@objc`. `.touchUpInside` is the case where the finger is lifted inside the button; other events can be chosen too. The action method can optionally take the button as a parameter. The action runs on the main thread, and writing to the screen is done there too.

#### The Measure

The core is the same as in the earlier lessons.

```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)")
    }
}
```

#### The Old and the Fixed Action

The model builds the two versions in the fragment in plain Swift. The model builds what the code writes to the screen; it is not the framework itself. Pressing the button is a direct call of the action method. In the fixed version the action writes the label too after updating the model.

```swift
// main.swift — the action forgets the label, the action writes the label too
final class CartStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class CartController {
    let store: CartStore
    let screen = Screen("CartController")

    init(store: CartStore) { self.store = store }

    func viewDidLoad() {
        screen.set("quantity", store.values["quantity"] ?? "")
    }

    func addTapped() {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
    }
}

final class FixedCartController {
    let store: CartStore
    let screen = Screen("FixedCartController")

    init(store: CartStore) { self.store = store }

    func viewDidLoad() {
        screen.set("quantity", store.values["quantity"] ?? "")
    }

    func addTapped() {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
        screen.set("quantity", String(next))
    }
}

let oldStore = CartStore()
oldStore.update("quantity", "1")
let old = CartController(store: oldStore)
old.viewDidLoad()
let oldOpened = check("opened-old", model: oldStore.values, screen: old.screen, keys: ["quantity"])
old.addTapped()
let oldTapped = check("tapped-old", model: oldStore.values, screen: old.screen, keys: ["quantity"])

let fixedStore = CartStore()
fixedStore.update("quantity", "1")
let fixed = FixedCartController(store: fixedStore)
fixed.viewDidLoad()
let fixedOpened = check("opened-fixed", model: fixedStore.values, screen: fixed.screen, keys: ["quantity"])
fixed.addTapped()
let fixedTapped = check("tapped-fixed", model: fixedStore.values, screen: fixed.screen, keys: ["quantity"])

countVerdicts(oldOpened + oldTapped + fixedOpened + fixedTapped)
```

```
opened-old | CartController.quantity: model=1 screen=1 -> matching
tapped-old | CartController.quantity: model=2 screen=1 -> stale
opened-fixed | FixedCartController.quantity: model=1 screen=1 -> matching
tapped-fixed | FixedCartController.quantity: model=2 screen=2 -> matching
matching: 3
stale: 1
missing: 0
leftover: 0
```

In the old version the screen is matching when it opens and `stale` after the touch: the model is 2, the screen is 1. In the fixed version the action writes both together, so it is matching after the touch too. The error is inside the action, in a single missing line, and it is invisible when the screen opens; it shows only after the touch.

For this reason, a useful question when reading action methods is this: directly below every line that changes the model, is there a line that also writes every screen field that looks at that value? If the answer is no, the screen falls behind the model after that action. The measurement asks the same question with a number: it reads the model and the screen side by side and marks the difference as `stale`. If the action affects more than one label, each label needs its own line; writing one and forgetting the other is a quieter form of the same error.

**VC1.** In the model, a touch is a direct call of the action method. In the real framework, if the button's target and action are not connected, a touch calls no method; this lesson does not measure that case.

#### Common Mistakes

- **Updating only the model.** The action changes the value and does not write the label. The screen stays stale, as measured in this lesson.
- **Writing one of several labels.** The action writes the total but forgets the quantity; half the screen is matching and half is stale.
- **Misspelling the action name.** If the name of the method connected in the file is changed in code, the link breaks. After a touch a run-time error occurs, or nothing happens.
- **Skipping the `@objc` mark.** A `#selector` given in code requires it; without the mark the code does not compile.
- **Connecting an action to the same button twice.** Two separate methods are called in turn, and if both write to the model, the value changes twice.
- **Comparing a formatted value with the model.** The label says `$120.00` while the model says `120`; someone reading the measurement may take this for an error. The comparison has to pass through the same format.

#### Reading the Measurement

Of the four lines, three are matching and one is stale. The stale line is the old action after the touch: the model is 2, the screen is 1. The opening line of the same screen is matching, and this shows where the error is: not in the screen's first setup, but after user interaction.

For this reason measuring a screen only at its first opening is not enough. While the opening line is matching, the interaction line can be stale. The fixed action moves the write call right after the model, so both lines are matching. The rule saying stale on formatted text is a separate warning: the result does not say the screen is wrong, it says the two values were written differently.

#### The Sender and Event Types

The action method can receive the button itself through the `sender` parameter. If the parameter's type is written as a concrete type instead of `Any`, the method reaches the button's properties directly. Controls other than buttons send actions too; the selecting event depends on the type of the control:

```swift
import UIKit

final class SettingsController: UIViewController {
    @IBOutlet weak var notificationsSwitch: UISwitch?
    @IBOutlet weak var nameField: UITextField?

    @IBAction func switchChanged(_ sender: UISwitch) {
        // sender.isOn is written to the model, then the related labels are refreshed
    }

    @IBAction func nameEdited(_ sender: UITextField) {
        // sender.text is written to the model
    }
}
```

The switch calls its action with the `valueChanged` event and the text field with the `editingChanged` event. More than one action can be connected to a control, and `removeTarget` breaks a link. How many times and for which event an action is called is inside the link in the file; looking at the method itself does not show it.

#### Three Steps: Read, Change, Write

An action method mostly follows the same order: it reads the input from the control, changes the model, and writes every field on the screen that depends on that model. When the third step is skipped, the staleness we measure in this lesson appears. One way to reduce the chance of skipping that step is to write all the fields that depend on the model in a single method:

```swift
import UIKit

extension CartController {
    func showModel() {
        quantityLabel?.text = store.values["quantity"] ?? ""
        totalLabel?.text = store.values["total"] ?? ""
    }

    @IBAction func addTapped(_ sender: Any) {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
        showModel()
    }
}
```

`viewDidLoad` and every action call the same `showModel` method. When a new field is added, the write call is added in one place and none of the actions can skip it. This arrangement does not remove the error; it reduces the number of places to call to one. The measurement also asks the same question after every action: are the model and the screen the same.

#### Outlet Types and Weak References

Outlets are mostly defined as `weak`. The main view tree already holds the view; the controller does not need to hold a strong reference as well. When the view is removed from the tree, the weak reference becomes `nil`. This is why the outlet type is optional and the read site is written with `?`.

An action can work without an outlet; but to reach a label from inside the action, an outlet is needed. The action method does its writing through the outlet. If the outlet is `nil`, the write is silently skipped, and this is the direct cause of the staleness this course measures.

#### Consistency in Action Names

The names of action methods should say what they do, not what they are. Writing `addItemTapped` or `saveTapped` instead of `buttonTapped` lets the person reading the link in the file understand what will run without opening the method. If the name of an action connected to a button in the file was changed, the link stays with the old name, and when the button is pressed the system cannot find the method.

If an action does more than one job, the jobs are split into small methods and the action calls these methods in turn. This separates the code that changes the model from the code that writes the screen; the measurement asks exactly whether these two layers agree.

If a control has no action connected, pressing it does nothing and gives no error. This is another way for a screen to stay stale: the model has not changed, and neither has the screen. The agreement measurement comes out `matching` in this case, but the user sees that the button does not work. For this reason the measurement alone is not enough evidence; whether every outlet and every action is really connected is checked separately. A small checklist in the controller is enough for this: for each control, is there an outlet, is there an action, does the action change the model, does it then write the screen. If all four questions are yes, the screen agrees with the model. If one is no, the problem is at that step, and the measurement result guides which step to look at.

#### The Bounding Measurement

The rule compares the model value with the screen text exactly. The screen often shows not the model itself but a formatted version of it: the model says `120`, the label writes `$120.00`. The two are the same information, but the rule does not know that.

```swift
// main.swift — formatted text
final class PriceStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

func formatPrice(_ value: String) -> String { "$" + value + ".00" }

final class PriceController {
    let store: PriceStore
    let screen = Screen("PriceController")

    init(store: PriceStore) { self.store = store }

    func viewDidLoad() {
        screen.set("price", formatPrice(store.values["price"] ?? ""))
    }
}

let store = PriceStore()
store.update("price", "120")
let controller = PriceController(store: store)
controller.viewDidLoad()
let raw = check("formatted-text", model: store.values, screen: controller.screen, keys: ["price"])
let formattedModel = ["price": formatPrice(store.values["price"] ?? "")]
let formatted = check("formatted-compared", model: formattedModel, screen: controller.screen, keys: ["price"])
countVerdicts(raw + formatted)
```

```
formatted-text | PriceController.price: model=120 screen=$120.00 -> stale
formatted-compared | PriceController.price: model=$120.00 screen=$120.00 -> matching
matching: 1
stale: 1
missing: 0
leftover: 0
```

The comparison with the raw value gave `stale`, while the screen was right. When the model was passed through the same formatting, the line came out matching. This is the rule's limit: it does not know about formatting. For this reason a `stale` finding does not say a screen is wrong; it says the two values were written differently, and deciding which is right is the job of the person reading the measurement.

#### Summary

- An outlet reaches from the controller to the view, an action from the view to the controller; the action is called through the target-action pattern.
- An action that updates the model but does not write the label leaves the screen stale.
- An action that writes both the model and the label is matching after the touch too.
- A controller that looks matching when the screen opens can be stale after a touch.
- Formatted text comes out stale when compared with the raw value; when both sides pass through the same format, it is matching.
- This lesson's agreement: **stale**.

#### Next Step

This lesson closed the view controller, the lifecycle, navigation, presentation and outlets. In the next lessons we will move on to the layout of the screen: we will cover describing the size and position of a view with rules in constraint-based layout.

## Layout and Collections

Constraint-based layout with constraints, priorities and ambiguity resolution, stack views that reduce the number of constraints, table views with cell reuse and a data source, collection views with a flexible layout definition, the delegate pattern for handing off responsibility, and low-level visual control with drawing and layer animation.

### Constraint-Based Layout

Source: https://academia.sh/en/courses/uikit/constraint-based-layout

A measurement of how a constraint constant that changes in the model but is never written to the on-screen constraint counts as stale, and a constraint that is never set up counts as missing.

The previous topic's last lesson, Outlets and Actions, showed how a view on screen is connected to code. In this topic, what gets connected is not the view itself but its **layout**. In UIKit, layout is usually given by a **constraint**: a rule that states how an edge or the width of a view is positioned relative to something else. A constraint carries a constant, and in code that constant is a number. When the model changes that number, the constraint on screen does not learn the new number by itself.

The question of this lesson is not whether a calculation is correct but **agreement**: the model changed a spacing or a width, did the screen take that value. The measurement uses the core file.

```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)")
    }
}
```

In the core, `Screen` keeps the values most recently written to the screen, one per key. `verdict` compares the model's value with the screen's value and returns one of four results: `matching` if both are the same, `stale` if they differ, `missing` if the model holds a value and the screen does not, and `leftover` if the screen holds a value and the model does not. The core is not a constraint solver; the model builds what the code writes to the screen, and it is not the framework itself.

#### The Constant a Constraint Carries

Consider the side padding and the width of an invoice card. Both are constants of constraints in code. When the card is created, the constraints are set up once and the constants take their values at that moment.

```swift
import UIKit

final class InvoiceCardView: UIView {
    let paddingConstraint = NSLayoutConstraint()
    let widthConstraint = NSLayoutConstraint()

    func apply(padding: CGFloat, width: CGFloat) {
        paddingConstraint.constant = padding
        widthConstraint.constant = width
    }
}
```

The `apply(padding:width:)` method here writes the two constants. The critical point is that the constants change only when this method is called. If the model later increases the padding and the method is not called, the constraint keeps carrying the old number.

In the measurement, the model carries two fields: `sidePadding` and `cardWidth`. The screen is a `Screen` that holds the constants written to the constraints. Three steps are given: the first setup, the model changing the padding, and the constant being updated.

```swift
// main.swift — the model changes the padding, it stays stale until the constraint is updated
let invoice = Screen("InvoiceCard")
var model = ["sidePadding": "16", "cardWidth": "320"]

invoice.set("sidePadding", "16")
invoice.set("cardWidth", "320")
let first = check("first-setup", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

model["sidePadding"] = "24"
let second = check("padding-changed", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

invoice.set("sidePadding", "24")
let third = check("constraint-updated", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

countVerdicts(first + second + third)
```

```
first-setup | InvoiceCard.sidePadding: model=16 screen=16 -> matching
first-setup | InvoiceCard.cardWidth: model=320 screen=320 -> matching
padding-changed | InvoiceCard.sidePadding: model=24 screen=16 -> stale
padding-changed | InvoiceCard.cardWidth: model=320 screen=320 -> matching
constraint-updated | InvoiceCard.sidePadding: model=24 screen=24 -> matching
constraint-updated | InvoiceCard.cardWidth: model=320 screen=320 -> matching
matching: 5
stale: 1
missing: 0
leftover: 0
```

In the `first-setup` rows both fields are matching: the model and the screen carry the same number. In the `padding-changed` row, the model has 24 and the screen has 16 for `sidePadding`, and the result is `stale`. The screen still holds the old constant; `cardWidth` stays matching because it never changed. In the `constraint-updated` step, once the constant is written, both fields are matching again.

The count comes from six checks: five matching, one stale. The only stale row is the step where the model changed but the constraint was not written. No row is missing or leftover, because both keys exist on both sides.

**LC1.** This measurement compares the constant as text equality. Whether the constraint was activated, or whether it conflicts with another constraint, was not measured.

#### A Constraint That Is Never Set Up

Stale means the screen carries a value but it has aged. A quieter case is the model carrying a value while the constraint that would receive it on screen was never set up. Suppose a footnote area is added to the bottom of the card: the model states its height, but the code has not yet created that height constraint.

```swift
// main.swift — the model gives a height, it counts as missing until the constraint is set up
let card = Screen("InvoiceCard")
let model = ["sidePadding": "24", "footerHeight": "44"]

card.set("sidePadding", "24")
let before = check("footnote-added", model: model, screen: card, keys: ["sidePadding", "footerHeight"])

card.set("footerHeight", "44")
let after = check("constraint-set", model: model, screen: card, keys: ["sidePadding", "footerHeight"])

countVerdicts(before + after)
```

```
footnote-added | InvoiceCard.sidePadding: model=24 screen=24 -> matching
footnote-added | InvoiceCard.footerHeight: model=44 screen=- -> missing
constraint-set | InvoiceCard.sidePadding: model=24 screen=24 -> matching
constraint-set | InvoiceCard.footerHeight: model=44 screen=44 -> matching
matching: 3
stale: 0
missing: 1
leftover: 0
```

In the `footnote-added` step, `footerHeight` has 44 in the model, `-` on the screen, and the result is `missing`. The screen has no value at all for this key. Once the constraint is written in the `constraint-set` step, the row becomes matching. The total is three matching and one missing; there is no stale row, because a wrong number never appeared on screen, the number was absent.

Missing and stale look similar in code: in both, the user sees a wrong layout. The difference lies in the cause. Stale says the write step was skipped; missing says the setup step was never written.

**LC2.** This measurement is for the case where the footnote area is represented by a single key. Whether part of a group of constraints was set up and part was not was not measured.

#### How Constraints Are Actually Solved

In the measurement a constraint was reduced to a single number, but the framework itself solves constraints as a system of equations. The position and size of each view are determined by four values: the left edge, the top edge, the width and the height. Constraints write the relationships among these values. A set of constraints that determines these four values for a view in a single unambiguous way is complete. If it falls short, **ambiguity** arises: the framework can place the view in two or more valid positions, and which one it picks is not decided by a rule. If it has too much, a **conflict** arises: two constraints ask for different values of the same quantity, and one of them is broken at run time.

The **priority** decides which constraint gets broken. A priority is a number between 1 and 1000; 1000 is a required constraint, and the framework looks for a solution without breaking it. A constraint with a lower priority gives way when it clashes with higher-priority constraints. The framework writes the constraint it broke to the output, but the screen is still drawn; for this reason a conflict often remains only a log line and goes unnoticed.

```swift
import UIKit

func pinCard(_ card: UIView, in container: UIView) {
    card.translatesAutoresizingMaskIntoConstraints = false
    let width = card.widthAnchor.constraint(equalToConstant: 320)
    width.priority = .defaultHigh
    NSLayoutConstraint.activate([
        card.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
        card.centerXAnchor.constraint(equalTo: container.centerXAnchor),
        card.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 16),
        width
    ])
}
```

In this fragment the width is given priority 750. When the container is narrow and the leading-edge constraint stays required, the width constraint gives way; when the container is wide, it applies as 320. A single set of constraints therefore fits both cases. The point to watch is turning off `translatesAutoresizingMaskIntoConstraints`: if this value is left on for a view built in code, the framework also adds the view's automatic constraints from the older layout style, and these conflict with the constraints you wrote.

#### Intrinsic Content Size and Resistance Values

A view can also carry a size without any constraints. A label asking for as much room as its text, or a button as much as its title, is an example; this request is called the **intrinsic content size**. For a view with an intrinsic content size, writing a width or height constraint is often unnecessary. Two values say what the view does when the space is too small or too large: **content hugging** states how strongly the view resists growing beyond its intrinsic content size, and **compression resistance** states how strongly it resists shrinking. When two labels sit side by side and the space is narrow, the label with the lower compression resistance gets shorter; if the two are equal, the framework cannot know which to pick, and ambiguity arises.

For this reason, for two variable-length texts placed side by side, one label must be given a clearly higher resistance. The `FlexibleCard` in the measurement showed that this flexibility is not reflected in the model: the resistance values are not in the model, yet they are decisive on screen.

#### Building the Constraint in Code or in the Editor

A constraint can be built in the visual interface editor or in code, and the two give the same result. The editor is short for structures that do not change: margins, fixed heights and alignments are drawn once and stored in the file. Code is useful for layouts that change at run time: a section appearing conditionally, a height that varies with the content, or a spacing that shifts with a user setting. If a constraint built in code is stored in a variable, its `constant` can be changed later and the framework solves the layout again in the next pass. The first two measurements of this lesson are about exactly this variable being updated. A simple rule is enough to decide: if the value will not change after setup, choose the editor; if it will change together with the model, choose code.

In mixed use, the only rule is that the constraints of a view come from a single source. If a constraint built in the editor must be changed in code, the constraint is connected to the code through an outlet, and the constant of the existing constraint is updated instead of adding a new one.

#### Common Mistakes

- Setting up a constraint and forgetting to activate it. A constraint that has been created does nothing until it is activated. Activation is done with `isActive` or with `NSLayoutConstraint.activate`.
- Setting up constraints for the same view in two separate places. If one comes from code and the other from the visual interface editor, a conflict arises and which one is broken can vary from run to run.
- Forgetting to update the constant of the constraint when the model changes. The first measurement of this lesson is exactly this case: the constant carries the value it had when it was set up and does not follow the model on its own, even if the model changes later.
- Leaving the constraints of a hidden view in place. Even when a view is hidden, its constraints stay and the gap is preserved; if you want the gap to close, a constraint change or a stack view is needed.
- Tying the priority number to an estimated value. If two constraints with exactly the same priority clash, the result is undefined; the priorities must be clearly separated from one another.

#### Interpreting the Measured Result

The stale result of the first measurement is not a bug of the framework; it follows from the definition of the constraint. When a constraint is stored as a value and not as a relationship, writing the code that updates the relationship is the developer's job. The missing result of the second measurement comes from the same root, but from the opposite direction: the model supplies a value and the constraint that would carry it on screen was never set up. The third measurement shows that priority is not in the model; the same constant gives the same agreement on both screens, and the difference in priority does not enter the output. The lesson to draw from the three results is this: when comparing the model with the screen, the value of a constraint can be seen, but its weight in the framework cannot.

#### The Bounding Measurement

Two properties of a constraint, **priority** and **ambiguity**, determine how the solver resolves clashes. A constraint with a low priority gives way when needed. In UIKit these two properties are read from code.

```swift
import UIKit

func makeWidthConstraint(for card: UIView, priority: UILayoutPriority) -> NSLayoutConstraint {
    let constraint = card.widthAnchor.constraint(equalToConstant: 320)
    constraint.priority = priority
    return constraint
}
```

The model has no such field. The model compares only the constant: whether the constraint is required or low-priority, the result is the same if the constant is the same. We show this with two screens that have the same constant; one is thought of as built with a required constraint, the other with a low-priority one. There is no key to carry the priority, so both write only `cardWidth`.

```swift
// main.swift — priority is not in the model, the same constant gives the same agreement on both screens
let model = ["cardWidth": "320"]
let required = Screen("RequiredCard")
let flexible = Screen("FlexibleCard")

required.set("cardWidth", "320")
flexible.set("cardWidth", "320")

let requiredResult = check("required", model: model, screen: required, keys: ["cardWidth"])
let flexibleResult = check("flexible", model: model, screen: flexible, keys: ["cardWidth"])

countVerdicts(requiredResult + flexibleResult)
```

```
required | RequiredCard.cardWidth: model=320 screen=320 -> matching
flexible | FlexibleCard.cardWidth: model=320 screen=320 -> matching
matching: 2
stale: 0
missing: 0
leftover: 0
```

Both rows are matching and the count is two matching. The difference in priority did not appear in the output. This is a limit: **the constraint solver is not measured**. The model compares the constant the constraint carries, not the size the screen gets after the solver. A constraint may have been overridden by a lower priority and the size on screen may still differ from the model; that difference is outside the scope of this lesson's measurement.

**LC3.** This measurement is only for the equality of the constant. Priority and ambiguity were shown in the SOURCE fragment and did not enter the output.

#### Summary

- In constraint-based layout, what the screen takes from the model is the constant of the constraint; when the model changes, the constant must be rewritten in code.
- A value that changes in the model but is not written to the constraint is stale: the screen carries the old number.
- A constraint that exists in the model but was never set up is missing: the screen has no value for that key.
- Priority and ambiguity are not in the model; the same constant gives the same agreement in a required or a flexible constraint.
- The model does not measure the constraint solver; it compares the constant, not the resolved size.
- This lesson's agreement: **stale**.

#### Next Step

Setting up constraints one by one becomes heavier as views multiply. In the next lesson a stack view takes the constraints on itself; what gets written is whether a subview is hidden or not. In that lesson the agreement is called by a new name: leftover.

### Stack Views

Source: https://academia.sh/en/courses/uikit/stack-views

A measurement of how a badge removed from the model but not hidden in the stack view counts as leftover, how it counts as matching once the hiding is written, and how hiding is built into the model as a visibility field.

In the previous lesson, Constraint-Based Layout, we measured that writing the constant of a constraint by hand lets the screen fall behind the model. Setting up constraints one by one becomes heavier as views multiply. A **stack view** lightens this load: it arranges its subviews along an axis and calculates the spacing between them itself. The measure of this lesson falls somewhere else. In a stack view, removing something from the screen means hiding it, and it is the code that does the hiding.

The measurement uses the core file.

```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)")
    }
}
```

The core compares every field shown on screen with the model's current value. The model builds what the code writes to the screen; it is not the framework itself.

#### A Subview That Drops Out of the Layout

Consider an order row: an order title on top, and next to it a badge that marks new orders. The two sit side by side in a stack view. When the badge is removed from the model, that is, when the order is no longer considered new, the badge's view keeps standing in the stack.

```swift
import UIKit

final class OrderRowView: UIView {
    let titleLabel = UILabel()
    let badgeLabel = UILabel()
    let rowStack = UIStackView()

    func configure(title: String, badge: String?) {
        titleLabel.text = title
        badgeLabel.text = badge
    }
}
```

When there is no badge, `configure(title:badge:)` does not empty the text, it only assigns the new value; and `badgeLabel.isHidden` is not set either. For this reason, when the badge is removed from the model, the old text stays on screen. The measurement has three steps: the order is loaded, the badge is removed from the model, and then the screen hides the badge.

```swift
// main.swift — a badge removed from the model is leftover if not hidden, out-of-scope once hidden
let row = Screen("OrderRow")
var model = ["title": "Order 1042", "badge": "New"]

row.set("title", "Order 1042")
row.set("badge", "New")
let loaded = check("order-loaded", model: model, screen: row, keys: ["title", "badge"])

model["badge"] = nil
let removed = check("badge-removed", model: model, screen: row, keys: ["title", "badge"])

row.clear("badge")
let hidden = check("badge-hidden", model: model, screen: row, keys: ["title", "badge"])

countVerdicts(loaded + removed + hidden)
```

```
order-loaded | OrderRow.title: model=Order 1042 screen=Order 1042 -> matching
order-loaded | OrderRow.badge: model=New screen=New -> matching
badge-removed | OrderRow.title: model=Order 1042 screen=Order 1042 -> matching
badge-removed | OrderRow.badge: model=- screen=New -> leftover
badge-hidden | OrderRow.title: model=Order 1042 screen=Order 1042 -> matching
badge-hidden | OrderRow.badge: model=- screen=- -> out-of-scope
matching: 4
stale: 0
missing: 0
leftover: 1
```

In the `order-loaded` step both fields are matching. In the `badge-removed` step `title` is still matching; for `badge` the model is `-`, the screen is `New` and the result is `leftover`. A value is left on screen that no longer has a counterpart in the model. In the `badge-hidden` step, once the screen is also `-`, the row prints `out-of-scope`: a field absent on both sides is counted as neither matching nor leftover.

The count of the six rows covers five: four matching, one leftover. The sixth row is `out-of-scope`, so it enters no count; none of the four names in the count includes it.

**LC1.** This measurement represents whether the badge exists on screen through the value of the key. Whether the stack view actually keeps the subview in the tree was not measured.

#### Hiding Is a Field

Hiding is a value written to the `isHidden` property of the subview. The stack view takes a hidden subview out of the layout and the remaining views close the gap. In other words, the screen's information about whether the badge is visible stands as a separate value, and the model can hold it as a visibility field.

```swift
import UIKit

extension OrderRowView {
    func showBadge(_ isVisible: Bool) {
        badgeLabel.isHidden = !isVisible
    }
}
```

In the model, the `badgeVisible` key carries the value `yes` or `no`. When the model wants the badge hidden and the screen does not receive that call, the two sides part ways.

```swift
// main.swift — visibility field: the model says hide, it stays stale if the screen does not hide
let row = Screen("OrderRow")
var model = ["badgeVisible": "yes"]

row.set("badgeVisible", "yes")
let shown = check("badge-visible", model: model, screen: row, keys: ["badgeVisible"])

model["badgeVisible"] = "no"
let stale = check("model-hid", model: model, screen: row, keys: ["badgeVisible"])

row.set("badgeVisible", "no")
let synced = check("screen-hid", model: model, screen: row, keys: ["badgeVisible"])

countVerdicts(shown + stale + synced)
```

```
badge-visible | OrderRow.badgeVisible: model=yes screen=yes -> matching
model-hid | OrderRow.badgeVisible: model=no screen=yes -> stale
screen-hid | OrderRow.badgeVisible: model=no screen=no -> matching
matching: 2
stale: 1
missing: 0
leftover: 0
```

In the second row the model is `no`, the screen is `yes` and the result is `stale`. When visibility stands as a value, skipping it yields stale, not leftover: both sides hold a value, only the screen's is old. The same subject was measured in two forms and two separate categories came out. When the field is removed from the model entirely it is leftover, when its value changes it is stale.

**LC2.** This measurement represents visibility with the texts `yes` and `no`. It was assumed that the `isHidden` value is a `Bool` and that the text comparison reflects what the code wrote to the screen.

#### Distribution in a Stack View

A stack view is a container that lines up its subviews along an axis and calculates the gaps between them itself. The axis is chosen with `axis`: horizontal or vertical. How the subviews spread along the axis is decided by the **distribution**, and the placement in the direction perpendicular to the axis is decided by the **alignment**. This distinction matters, because the two settings are independent of each other and this is the point that is mixed up most often.

Distribution has five values. `fill` fills the space by stretching one of the subviews; which one stretches is told by the hugging resistances. `fillEqually` gives all of them equal size. `fillProportionally` distributes in proportion to the intrinsic content sizes. `equalSpacing` equalizes the gaps in between without touching the sizes. `equalCentering` equalizes the distance between the centers of the views. Alignment, in turn, chooses the position in the perpendicular direction with values such as `fill`, `leading`, `center` and `trailing`.

```swift
import UIKit

func makeRow(title: String, badge: UIView) -> UIStackView {
    let label = UILabel()
    label.text = title
    let row = UIStackView(arrangedSubviews: [label, badge])
    row.axis = .horizontal
    row.alignment = .center
    row.distribution = .fill
    row.spacing = 8
    return row
}
```

Since the distribution in this row is `fill`, one of the views between the label and the badge has to stretch. If it is not stated explicitly which one will stretch, the framework looks at the intrinsic content size and the resistances. To keep the badge at a fixed size, raising the badge's hugging resistance is enough.

#### Hiding and Removing Are Not the Same

The most useful behavior of a stack view is that it closes the place of a hidden subview. A view whose `isHidden` value is set to `true` leaves the layout and its neighbors fill the gap. This behavior is specific to the stack view; a view hidden outside a stack keeps its place.

Removing is a separate operation. The `removeArrangedSubview` call takes the view out of the distribution only; the view is still a subview of the stack view and continues to be drawn. To remove it from the screen for real, `removeFromSuperview` is needed. This difference is the subject of the third measurement of this lesson: hiding and removing write the same visibility value in the model, but they give different results in terms of size and layout.

```swift
import UIKit

func hide(_ badge: UIView, in row: UIStackView) {
    badge.isHidden = true
}

func remove(_ badge: UIView, from row: UIStackView) {
    row.removeArrangedSubview(badge)
    badge.removeFromSuperview()
}
```

The second function takes two steps because only the first leaves the view on screen. Only the second also takes it out of memory; if the view needs to be shown again later, hiding is cheaper.

#### Stack View or Constraints

A stack view is not the answer to every layout problem. Where subviews line up along a single axis, that is, in forms, rows, button bars and label lists, a stack view reduces dozens of constraints to a single setting. If views overlap, if a view is positioned relative to the corner of another view, or if proportions are tied to each other, constraints are more direct. Combining the two is also common: the stack view itself is positioned with constraints, and the ordering inside it is left to the stack view.

This choice matters for the measurement too. Because the stack view resolves the layout itself, the match between the model and the screen comes down to the existence and visibility of the subview. In constraint-based layout, each constant has to be updated separately. Building the same screen in two ways also changes the number of values that may stay stale.

#### Common Mistakes

- Adding directly instead of through the arranged subviews of the stack view. A view added to a stack view directly with `addSubview` does not take part in the distribution.
- Confusing hiding with removing. If only `removeArrangedSubview` is called when the badge is removed from the model, the badge stays on screen; the leftover row in the first measurement of this lesson is this case.
- Confusing distribution with alignment. A person who wants to center the views horizontally in a vertical stack changes the `distribution` value and gets no result.
- Nesting stack views deeper than necessary. Each layer means one more constraint resolution and makes debugging harder.
- Writing the gap into the subview's own constraints. A stack view does this with `spacing`; if it is also written as a constraint, a conflict arises.

#### Interpreting the Measured Result

In the first measurement the badge was absent from the model and still on screen, and the row counted as leftover. The stack view does not clear this by itself, because the container does not know which subview matches the model. A badge goes away in two ways: it is hidden or it is removed. When the badge was hidden or removed, the row became out-of-scope, because there was no value on either side. In the third measurement hiding and removing wrote the same visibility value and both came out matching: the measuring axis is concerned with whether a value is present on screen, not with whether the place of the view closes. The second measurement showed another break in the visibility field: if the model said hide and the screen did not hide, the row is stale. Reading the three measurements together gives a single conclusion about the stack view: the container resolves the layout, but the developer ensures the match with the model.

In practice this requires two separate checks on a screen that uses a stack view. First, for every item in the model, it is checked whether the screen has a subview; if one is missing, it has not been added. Then, for every subview on the screen, a counterpart is looked for in the model; a view with no counterpart, that is, one left over, has not been hidden or removed. During removal too, taking a view out of the distribution and removing it from the view tree are separate steps, and unless both are done the view stays on screen. The stack view does not do this work for us, because the container does not know which subview belongs to which model item. Collecting these two checks in a helper function ensures that the same check runs on every update of the screen and noticeably reduces the number of forgotten cases; the rule is written once and not remembered again on every screen. Moreover, if this check is run after every update for conditionally appearing items such as the badge, stale and leftover rows become visible the moment they first appear.

#### The Bounding Measurement

We can take a subview out of a stack in two ways: we can hide it or we can remove it from the stack. The gap each leaves in the layout may not be the same; a hidden view does not reserve room in the stack's arrangement but is still in the tree, while a removed one has left the tree. **A hidden subview drops out of the stack layout**; this is a consequence of the layout, and the model does not measure it.

The model builds only visibility as a field. Both ways write the same value to the same field, so no claim is made about the size difference.

```swift
// main.swift — hiding and removing write the same visibility value, the size difference is not measured
let model = ["badgeVisible": "no"]
let hiddenRow = Screen("HiddenRow")
let removedRow = Screen("RemovedRow")

hiddenRow.set("badgeVisible", "no")
removedRow.set("badgeVisible", "no")

let hiddenResult = check("hidden", model: model, screen: hiddenRow, keys: ["badgeVisible"])
let removedResult = check("removed", model: model, screen: removedRow, keys: ["badgeVisible"])

countVerdicts(hiddenResult + removedResult)
```

```
hidden | HiddenRow.badgeVisible: model=no screen=no -> matching
removed | RemovedRow.badgeVisible: model=no screen=no -> matching
matching: 2
stale: 0
missing: 0
leftover: 0
```

The two rows give exactly the same result: matching. The difference that hiding and removing leave in the layout did not enter the output, because the model has no size field. This is a limit: the model does not say whether a subview dropped out of the layout, it says whether the visibility field was written on screen.

**LC3.** This measurement tests that two screens carry the same value. No number was written for how the stack view closes the gap.

#### Summary

- A stack view arranges subviews; a hidden subview drops out of the layout, and it is the code that writes the hiding.
- A field removed from the model but not hidden on screen results in leftover.
- A field absent on both sides is out-of-scope and enters no count.
- When visibility is held as a field, a skipped hiding gives stale, and a field removed entirely gives leftover.
- Hiding and removing write the same visibility value in the model; the size difference is not measured.
- This lesson's agreement: **leftover**.

#### Next Step

Stack views arrange subviews once. A table view, on the other hand, uses the same cell many times, and a reused cell can carry the trace of the previous row. In the next lesson the reuse of the cell and the value left over from the old row will be measured.

### Table Views

Source: https://academia.sh/en/courses/uikit/table-views

A measurement of how a note of the previous row left in a reused cell counts as leftover, how a row whose model changed but was not reloaded counts as stale, and how which cell comes back is not measured.

In the previous lesson, Stack Views, we measured that removing something on screen means hiding it and that the code has to write the hiding. A **table view** carries another form of the same load. In a long list a separate view is not produced for every row; a **cell** that leaves the screen is **reused** for another row. If the cell is handed back without being cleaned, everything left over from the previous row stands on top of the new row.

The measurement uses the core file.

```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)")
    }
}
```

The core compares every field shown on screen with the model's current value. A cell here is a `Screen`, and the same `Screen` object is rewritten from row to row; the model builds what the code writes to the screen, and it is not the framework itself.

#### The Reused Cell

Consider a contact list. Each row shows a name; next to favorite contacts a note is also shown. A table view asks for the cell with a **reuse identifier**. The cell may be newly produced or it may have been used before.

```swift
import UIKit

final class ContactCell: UITableViewCell {
    func configure(name: String, note: String?) {
        textLabel?.text = name
        if let note {
            detailTextLabel?.text = note
        }
    }
}

func cellForRow(_ tableView: UITableView, at indexPath: IndexPath, contact: Contact) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
    cell.configure(name: contact.name, note: contact.note)
    return cell
}
```

`configure(name:note:)` writes the note only if there is one; if there is none, it leaves the field as it is. For this reason, if a row without a note inherits the cell of a row with a note, the old note stays on screen. The measurement has three steps: the first row is written, the cell is reused for the second row, and then the cell is cleaned.

```swift
// main.swift — the previous row's note left in a reused cell is leftover, out-of-scope once cleaned
let cell = Screen("ContactCell")
let rowOne = ["name": "Ayse", "note": "Favorite"]
let rowTwo = ["name": "Mehmet"]

cell.set("name", "Ayse")
cell.set("note", "Favorite")
let first = check("row-one", model: rowOne, screen: cell, keys: ["name", "note"])

cell.set("name", "Mehmet")
let reused = check("reused", model: rowTwo, screen: cell, keys: ["name", "note"])

cell.clear("note")
let cleaned = check("preparation-cleared", model: rowTwo, screen: cell, keys: ["name", "note"])

countVerdicts(first + reused + cleaned)
```

```
row-one | ContactCell.name: model=Ayse screen=Ayse -> matching
row-one | ContactCell.note: model=Favorite screen=Favorite -> matching
reused | ContactCell.name: model=Mehmet screen=Mehmet -> matching
reused | ContactCell.note: model=- screen=Favorite -> leftover
preparation-cleared | ContactCell.name: model=Mehmet screen=Mehmet -> matching
preparation-cleared | ContactCell.note: model=- screen=- -> out-of-scope
matching: 4
stale: 0
missing: 0
leftover: 1
```

In the `row-one` step both fields are matching. In the `reused` step `name` is matching because the name was rewritten; for `note` the model is `-`, the screen is `Favorite` and the result is `leftover`. The new row's model has no note, but the cell carries the previous row's note. In the `preparation-cleared` step, once the field is cleared, the row prints `out-of-scope` and is not counted.

The count covers five of the six rows: four matching, one leftover. The step where the cell is reused is the only place where the leftover is born.

**LC1.** This measurement is for a sequence of two rows. No general rule was claimed that a cell must be cleaned on every reuse; what is measured is what the code leaves on the screen.

On the SOURCE side, the cell itself can do the cleaning. Just before handing the cell back from the queue, the table view calls its `prepareForReuse()` method; by overriding this method, state unrelated to the content is reset, while the content is rewritten every time in `cellForRowAt`.

```swift
import UIKit

extension ContactCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        detailTextLabel?.text = nil
    }
}
```

The `preparation-cleared` step of the previous measurement represents the result of this cleaning: the note field is emptied, and since the model carries no note either, the leftover disappears.

#### The Model Changes, the Row Is Not Refreshed

A wrongly kept cell is not the only cause. Sometimes the cell is configured correctly and the model changes afterward. A table view does not know by itself that the model changed; the reload of the row has to be called.

```swift
import UIKit

func contactRenamed(_ tableView: UITableView, row: Int) {
    tableView.reloadRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
}
```

If `reloadRows(at:with:)` is called here, the table asks again for the cell of that row and `configure` runs once more. If it is not called, the cell keeps showing the old name. The measurement builds these two cases as separate steps.

```swift
// main.swift — the model changes the row name, it stays stale until the row is reloaded
let cell = Screen("ContactCell")
var model = ["name": "Mehmet"]

cell.set("name", "Mehmet")
let before = check("row-showing", model: model, screen: cell, keys: ["name"])

model["name"] = "Mehmet Ali"
let stale = check("model-changed", model: model, screen: cell, keys: ["name"])

cell.set("name", "Mehmet Ali")
let reloaded = check("row-reloaded", model: model, screen: cell, keys: ["name"])

countVerdicts(before + stale + reloaded)
```

```
row-showing | ContactCell.name: model=Mehmet screen=Mehmet -> matching
model-changed | ContactCell.name: model=Mehmet Ali screen=Mehmet -> stale
row-reloaded | ContactCell.name: model=Mehmet Ali screen=Mehmet Ali -> matching
matching: 2
stale: 1
missing: 0
leftover: 0
```

In the first step it is matching; in the second step the model is `Mehmet Ali`, the screen is `Mehmet` and the result is `stale`. In the third step the row was reloaded and it is matching. The count is two matching, one stale. Reuse produced leftover, and when the reload was skipped the result was stale: two different breaks in the same list, two different categories.

**LC2.** This measurement covers a single cell and a single field. Several rows changing at the same time was not measured.

#### Cell Registration and Reuse

A table view can show hundreds of rows, but it builds only as many cells as fit on the screen at the same time. A cell that leaves the screen is not thrown away; it goes into a pool and is used again for a new row. For this reason the cell is obtained in two steps. First the cell class is registered with the table under a **reuse identifier**; then, on every row, the data source asks the pool for a cell with this identifier.

```swift
import UIKit

final class ContactListController: UITableViewController {
    var contacts: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ContactCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        contacts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath)
        cell.textLabel?.text = contacts[indexPath.row]
        return cell
    }
}
```

There are two required data source methods: the one that says how many rows there are and the one that builds the cell of each row. Because the second is called every time, it has to write all the fields of the cell. A cell coming from the pool carries the values left from the previous row; if a field is not written, the old value shows.

#### Preparing the Cell

A cell returning to the pool gets a chance to clean itself. The cell class can override the `prepareForReuse` method to reset state unrelated to the content (selection, opacity, editing mode). The content itself is always rewritten in `cellForRowAt`; that is the safe place to erase the row's leftover.

```swift
import UIKit

final class NoteCell: UITableViewCell {
    let noteLabel = UILabel()

    override func prepareForReuse() {
        super.prepareForReuse()
        noteLabel.text = nil
        accessoryType = .none
    }
}
```

Two fields are emptied here: the note text and the accessory icon. If a field is skipped, its value appears in the next row. Cleaning the cell and the assignment in the data source complement each other; relying on only one of them is not enough.

#### When the Data Changes, the Row Is Not Refreshed

A table view does not follow the content of the data source by itself. When the array changes, the screen stays the same until the table is told. There are two ways to tell it: `reloadData` reloads the whole table, and `reloadRows(at:with:)` reloads only the specified rows. If the name of a single row changed, the second is cheaper and preserves the scroll position. Adding or deleting rows is done with `insertRows` and `deleteRows`; calling these without changing the data array at the same time leads to an inconsistency error at run time.

#### Table View or Collection View

A table view is designed for single-column, vertically scrolling lists and brings list behaviors ready-made, such as headers, footers, editing mode and swipe-to-delete. The rows in a settings screen, a contact list or an order history are typical examples. If the items spread across several columns, scroll horizontally, or the row layout varies from item to item, a collection view is more suitable. A collection view also supports the list layout, so the difference between the two has gradually shrunk in newly built screens; but a table view still needs less code for simple lists.

The concept of reuse is the same in both. The cell comes from the pool, so the code that configures the cell has to write every field. The leftover rows of this lesson will meet you whichever view you choose.

#### Common Mistakes

- Asking the pool for a cell without registering it. A request made with an unregistered identifier stops the app at run time.
- Not writing every field of the cell. A cell coming from the pool carries the values of the old row; this is the leftover row in the first measurement of this lesson.
- Keeping persistent state in the cell. A cell is part of the screen at that moment, not of the row; state such as being selected should be kept in the data source.
- Doing heavy work inside the cell. During scrolling every new row calls `cellForRowAt`, and long-running work breaks fluidity.
- Changing the data array and not telling the table. The name of the row changes, but the screen keeps showing the old name; this is the stale result of the second measurement.

#### Interpreting the Measured Result

The common point of the three measurements is that the cell and the data live separately. In the first measurement the cell carried the previous row's note, and since the model held no note in that row, the result was leftover. Once the cell was cleaned, the row became out-of-scope, because there was no value on either side. In the second measurement the model changed but the table was not told, the screen stayed old, and the result was stale. In the third measurement, which cell came back was not measured: the same residue gives the same result. This shows that reuse is a silent behavior. There is no error, there is no warning; there is only an old value, and the only way to find it is to compare it with the model.

A practical rule can be drawn here: if the code that configures the cell does not write a field, it must empty that field. Every line that skips a field carries the risk of showing again the previous value of the cell coming from the pool, and because this risk appears only during scrolling, in a particular row order, it can slip past testing.

#### The Bounding Measurement

Reuse has a numeric side too: how many cells the table view keeps and which cell returns to which row. The measure of this lesson does not cover these. **The queue size and which cell comes back are not measured.** The model only builds two configurations of one cell in sequence and looks at the last values written.

To show this, we build the same residue in two different cells: one is the same cell, the other is a different cell. For the rule there is no difference between them.

```swift
// main.swift — which cell comes back is not measured, the same residue gives the same result
let model = ["name": "Mehmet"]
let sameCell = Screen("SameCell")
let otherCell = Screen("OtherCell")

sameCell.set("name", "Mehmet")
sameCell.set("note", "Favorite")
otherCell.set("name", "Mehmet")
otherCell.set("note", "Favorite")

let sameResult = check("same-cell", model: model, screen: sameCell, keys: ["name", "note"])
let otherResult = check("other-cell", model: model, screen: otherCell, keys: ["name", "note"])

countVerdicts(sameResult + otherResult)
```

```
same-cell | SameCell.name: model=Mehmet screen=Mehmet -> matching
same-cell | SameCell.note: model=- screen=Favorite -> leftover
other-cell | OtherCell.name: model=Mehmet screen=Mehmet -> matching
other-cell | OtherCell.note: model=- screen=Favorite -> leftover
matching: 2
stale: 0
missing: 0
leftover: 2
```

The result for the two cells is exactly the same: the name is matching, the note is leftover. The count is two matching, two leftover. Which cell the residue came from did not enter the output, because the model has no field that carries the identity of the cell. This is a limit: the model compares the final state of the screen, not which cell led to that state.

**LC3.** This measurement tests that two screen objects carry the same values. No number was written for how many cells the table view keeps in the queue.

#### Summary

- A table view hands the cell back through the reuse identifier; the returning cell can carry the values of the previous row.
- A value absent from the new row's model but left in the cell is leftover; if it is cleaned while the cell is prepared, it falls out of scope.
- If a row whose model changed is not reloaded, the screen stays stale.
- Reuse produces leftover, a skipped reload produces stale; two separate breaks are in two separate categories.
- The queue size and which cell comes back are not measured; the model compares the last values written.
- This lesson's agreement: **leftover**.

#### Next Step

A table view is a single-column list. In the next lesson a collection view arranges items in two dimensions, and when an item is added to the data the screen has to learn about it. In that lesson a new agreement will be seen: missing.

### Collection Views

Source: https://academia.sh/en/courses/uikit/collection-views

A measurement of how an item added to the data source but not announced to the collection view counts as missing, how it counts as matching once a snapshot is applied, and how the position calculation is not measured.

In the previous lesson, Table Views, we measured that a reused cell can carry the trace of the previous row and that a row has to be reloaded separately when the model changes. A **collection view** carries the same idea into two dimensions: items are arranged in a grid or in another flexible layout. The measure of this lesson looks at the number of items. Whether the screen sees an item added to the data source is decided by a separate notification.

The measurement uses the core file.

```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)")
    }
}
```

The core compares every field shown on screen with the model's current value. In this lesson each item is a key: the identity of the item is the key and its title is the value. The model builds what the code writes to the screen; it is not the framework itself.

#### An Added Item May Not Reach the Screen

Consider a task board. The board shows the tasks in a collection view. The array of tasks stands in the **data source**; the collection view asks for the item count and the cells from there. When a task is added to the array, the view does not know it by itself.

```swift
import UIKit

final class BoardController: UIViewController, UICollectionViewDataSource {
    var tasks = ["Report", "Test"]
    @IBOutlet var collectionView: UICollectionView!

    func addTask(_ title: String) {
        tasks.append(title)
        collectionView.insertItems(at: [IndexPath(item: tasks.count - 1, section: 0)])
    }
}
```

`addTask(_:)` appends to the array and then calls `insertItems(at:)`. Without that call, the collection view keeps working with the old item count. The measurement has three steps: the board opens, the model receives the third task, and the screen writes it.

```swift
// main.swift — an item added to the data source stays missing until it is announced
let board = Screen("TaskBoard")
var model = ["task1": "Report", "task2": "Test"]

board.set("task1", "Report")
board.set("task2", "Test")
let opened = check("opening", model: model, screen: board, keys: ["task1", "task2", "task3"])

model["task3"] = "Release"
let added = check("item-added", model: model, screen: board, keys: ["task1", "task2", "task3"])

board.set("task3", "Release")
let notified = check("addition-notified", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(opened + added + notified)
```

```
opening | TaskBoard.task1: model=Report screen=Report -> matching
opening | TaskBoard.task2: model=Test screen=Test -> matching
opening | TaskBoard.task3: model=- screen=- -> out-of-scope
item-added | TaskBoard.task1: model=Report screen=Report -> matching
item-added | TaskBoard.task2: model=Test screen=Test -> matching
item-added | TaskBoard.task3: model=Release screen=- -> missing
addition-notified | TaskBoard.task1: model=Report screen=Report -> matching
addition-notified | TaskBoard.task2: model=Test screen=Test -> matching
addition-notified | TaskBoard.task3: model=Release screen=Release -> matching
matching: 7
stale: 0
missing: 1
leftover: 0
```

In the `opening` step `task3` is absent on both sides and prints `out-of-scope`; the other two are matching. In the `item-added` step the model for `task3` is `Release`, the screen is `-` and the result is `missing`: the data source knows the item, the screen does not. In the `addition-notified` step, once the item is written, all three rows are matching.

The count covers eight of the nine rows: seven matching, one missing. Missing is born here at the level of one item: a task that exists in the model and is not on the screen at all.

**LC1.** This measurement was made with three tasks, and the added item stands at the end. Adding the item in the middle of the array was not measured.

#### A Snapshot Writes Everything Together

Calling `insertItems(at:)` one by one requires mapping the indexes by hand once deletion and moving are added. A **snapshot** lifts this burden: the data source is given the new list of all items, and the view finds the difference itself.

```swift
import UIKit

func applyBoard(_ tasks: [String], to dataSource: UICollectionViewDiffableDataSource<Int, String>) {
    var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
    snapshot.appendSections([0])
    snapshot.appendItems(tasks)
    dataSource.apply(snapshot)
}
```

Here the insertion and the deletion reach the screen with a single call. In the measurement, let the model add one task and remove one task at the same time. Until the snapshot is written, neither has reached the screen. The helper function rewrites all the items on the screen according to the model.

```swift
// main.swift — a snapshot applies an addition and a removal together
func applySnapshot(_ items: [String: String], to screen: Screen) {
    for key in screen.shown.keys.sorted() where items[key] == nil {
        screen.clear(key)
    }
    for (key, value) in items.sorted(by: { $0.key < $1.key }) {
        screen.set(key, value)
    }
}

let board = Screen("TaskBoard")
let model = ["task2": "Test", "task3": "Release"]

board.set("task1", "Report")
board.set("task2", "Test")
let before = check("snapshot-before", model: model, screen: board, keys: ["task1", "task2", "task3"])

applySnapshot(model, to: board)
let after = check("snapshot-after", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(before + after)
```

```
snapshot-before | TaskBoard.task1: model=- screen=Report -> leftover
snapshot-before | TaskBoard.task2: model=Test screen=Test -> matching
snapshot-before | TaskBoard.task3: model=Release screen=- -> missing
snapshot-after | TaskBoard.task1: model=- screen=- -> out-of-scope
snapshot-after | TaskBoard.task2: model=Test screen=Test -> matching
snapshot-after | TaskBoard.task3: model=Release screen=Release -> matching
matching: 3
stale: 0
missing: 1
leftover: 1
```

Before, `task1` has the model `-` and the screen `Report`, the result is `leftover`; `task3` has the model `Release`, the screen `-`, the result `missing`; `task2` is matching. After, `task1` is absent on both sides, `out-of-scope`; `task3` and `task2` are matching. The count is three matching, one missing, one leftover. The same snapshot closed two different breaks together.

**LC2.** This measurement counts the `applySnapshot` helper as the screen side of the model; whether the real `apply` call ran animated or not animated was not measured.

#### Data Source and Snapshot

A collection view is more flexible than a table view: it shows an **item** instead of a row and can place items in a grid, in columns or in any layout the author designs. How the data is carried to the screen is done in one of two ways. The first is to write the count and cell methods as in a table; the second is to build a **diffable data source**.

A diffable data source states what will be on the screen with a **snapshot**. A snapshot is a value that carries the sections and the item identifiers in each section. When a new snapshot is applied, the data source calculates the difference between the old and the new and adds, deletes or moves only the changed items. In this way there is no need to call `reloadData` or to calculate row indexes by hand.

```swift
import UIKit

enum BoardSection {
    case main
}

final class TaskBoardController: UIViewController {
    var dataSource: UICollectionViewDiffableDataSource<BoardSection, String>!

    func apply(titles: [String]) {
        var snapshot = NSDiffableDataSourceSnapshot<BoardSection, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(titles)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}
```

There are two type parameters: the section identifier and the item identifier. Both have to be `Hashable`. An identifier carries the **identity** of the item, not its value; for this reason two items sharing the same identifier lead to an error at run time. If the identifiers are not unique, the snapshot cannot be built.

#### Cell Registration

In a collection view the cell is registered with a registration object. The registration object declares how the cell is configured with a closure, and the data source uses this object in the cell provider. In this way the identifier strings disappear, and the problem of a misspelled identifier crashing at run time goes away too.

```swift
import UIKit

func makeDataSource(for collectionView: UICollectionView) -> UICollectionViewDiffableDataSource<BoardSection, String> {
    let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, title in
        var content = cell.defaultContentConfiguration()
        content.text = title
        cell.contentConfiguration = content
    }
    return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, title in
        collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: title)
    }
}
```

The cell provider is called for every visible item and receives the item identifier. The path from the identifier to the content lies inside the closure; here the identifier is used directly as the title, but in a real application the identifier is the identifier of a model and the content is read from a separate source.

#### The Layout Is a Separate Object

How a collection view places its items is kept in the **layout** object. The data source and the layout are independent of each other: the same snapshot can be shown with both a grid layout and a list layout. The third measurement of this lesson verifies this; the grid board and the list board carry the same identifiers and the layout difference does not enter the output.

#### Choosing a Layout

The layout of a collection view determines how flexible the view will be. The flow layout lines items up row by row and is enough for fixed-size grids. The compositional layout gives a separate size description to sections, groups and items; screens of mixed shapes, horizontally scrolling strips and sections with headers are built with this layout. The list layout brings the behaviors of the table view to the collection view. The choice of layout does not affect the data source; the same snapshot can be shown with all three layouts.

This separation is the real gain of the snapshot approach. The data stays in one place, the form of the view in another, and the two do not know each other. Changing the layout of the screen does not require touching the data; changing the data does not require touching the layout.

#### Common Mistakes

- Using the identifier as a value. The displayed text of an item can change; the identifier must not change. A data source that uses the text as the identifier counts an item as deleted and added again when the text changes.
- Adding the same identifier twice. A snapshot does not accept a duplicate identifier.
- Building the snapshot from scratch every time and forgetting to add the items again. A missing item disappears from the screen.
- Applying a snapshot when the content of an existing item changes and expecting a difference to appear. When the identifier stays the same, the data source sees no difference; to refresh the content, a reload is requested separately.
- Mixing the layout and the data source in the same place. Writing layout information into the cell provider makes maintenance harder.

#### Interpreting the Measured Result

In the first measurement the third task entered the model but was not announced to the data source: in the `item-added` step the model was `Release` while the screen was empty and the row came out as missing. When the addition was announced, the same row became matching. In the second measurement two differences existed together: the first task had been removed from the model but stood on the screen, which was a leftover row; the third task was missing. After the snapshot was applied, the first task was absent on both sides and stayed out-of-scope, and the third task became matching. The count gave three matching, one missing and one leftover; only the state of the first step entered the count. The third measurement showed that the layout does not affect the agreement: the grid and the list gave the same four rows as matching, and the column count did not enter the output.

The three results lead to the same point. A diffable data source calculates the difference, but giving the snapshot, which is the input of the difference, at every change is the developer's job. As long as no snapshot is applied, the model and the screen live apart: an added item stays missing and a deleted item stays leftover.

The practical rule is this: whenever the model changes, a new snapshot is built and applied. Tying the snapshot to the model change itself reduces the chance of forgetting; updating it in a separate place is the main source of the changes that leave the screen stale.

#### The Bounding Measurement

The essence of a collection view is the flexible layout: items can line up in three columns or in a single column. Which item falls on which row and column is the calculation of the layout. **The position calculation of a flexible layout is not measured**; the model has no position key, and the key is only the identity of the item.

Let two boards carry the same items: think of one as a three-column grid and the other as a single-column list. The column count is not a field in the model, so the two give the same result.

```swift
// main.swift — the column count is not in the model, two layouts give the same result
let model = ["task1": "Report", "task2": "Test"]
let grid = Screen("GridBoard")
let list = Screen("ListBoard")

for (key, value) in model.sorted(by: { $0.key < $1.key }) {
    grid.set(key, value)
    list.set(key, value)
}

let gridResult = check("grid", model: model, screen: grid, keys: ["task1", "task2"])
let listResult = check("list", model: model, screen: list, keys: ["task1", "task2"])

countVerdicts(gridResult + listResult)
```

```
grid | GridBoard.task1: model=Report screen=Report -> matching
grid | GridBoard.task2: model=Test screen=Test -> matching
list | ListBoard.task1: model=Report screen=Report -> matching
list | ListBoard.task2: model=Test screen=Test -> matching
matching: 4
stale: 0
missing: 0
leftover: 0
```

All four rows are matching and the count is four matching. The difference between the grid and the list did not enter the output. This is a limit: the model says whether an item is on the screen, not where it stands.

**LC3.** This measurement tests that two boards carry the same key set. No number was written for how the items are distributed across columns.

#### Summary

- A collection view takes its items from the data source; an insertion has to be announced separately.
- An item added to the data source but not written to the screen is missing.
- A snapshot applies an addition and a removal together; the earlier leftover and missing close in a single step.
- An item that never existed and is absent on both sides is out-of-scope.
- The position calculation of a flexible layout is not measured; the model only compares whether an item is on the screen.
- This lesson's agreement: **missing**.

#### Next Step

Table views and collection views do not keep the data and the behavior themselves; they consult another object. In the next lesson this consulting relationship, the delegate pattern, will be covered. When a weakly held link falls away, the screen can stay stale.

### The Delegate Pattern

Source: https://academia.sh/en/courses/uikit/the-delegate-pattern

A measurement of how a weakly held delegate that is not wired or has been released drops the call and leaves the screen stale, and how not being wired and a method that writes nothing give the same result.

In the previous lesson, Collection Views, we saw that a view does not keep the data and the behavior itself but consults another object. The name of that consulting relationship is the **delegate** pattern. An object hands part of its work to another object that conforms to a **protocol**: when a selection is made, when a save finishes or when a row is tapped, the delegate's method is called. If the delegate is not wired, the call goes nowhere, and this is a silent break.

The measurement uses the core file.

```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)")
    }
}
```

The core compares every field shown on screen with the model's current value. In this lesson, pure Swift code made of a picker and the delegates that listen to it is also measured. This code contains no platform framework, so it really runs. The model builds what the code writes to the screen; it is not the framework itself.

#### A Weakly Wired Delegate

On a payment screen, a picker provides the delivery address. The picker informs the delegate when an address is chosen; the screen's job is to show this address. The delegate is held in the picker with a **weak reference**: if the picker kept the screen alive and the screen kept the picker alive, the two would enter a **retain cycle** in which neither releases the other. A weak reference prevents this, but it has a cost: if the delegate is released, the reference becomes `nil`.

```swift
// AddressPicker.swift — the address picker and the delegates that listen to it
protocol AddressPickerDelegate: AnyObject {
    func picker(_ picker: AddressPicker, didSelect address: String)
}

final class AddressPicker {
    weak var delegate: AddressPickerDelegate?

    func choose(_ address: String) {
        delegate?.picker(self, didSelect: address)
    }
}

final class CheckoutScreen: AddressPickerDelegate {
    let screen: Screen

    init(_ screen: Screen) { self.screen = screen }

    func picker(_ picker: AddressPicker, didSelect address: String) {
        screen.set("address", address)
    }
}

final class SilentCheckout: AddressPickerDelegate {
    func picker(_ picker: AddressPicker, didSelect address: String) {}
}
```

`choose(_:)` calls the delegate with optional chaining: if the delegate is `nil`, nothing happens and no error appears. `CheckoutScreen` writes the chosen address to the screen. `SilentCheckout` conforms to the protocol but its method is empty. In the first measurement the picker is first used without being wired, then it is wired.

```swift
// main.swift — a delegate that is not wired leaves the screen stale, it becomes matching once wired
let screen = Screen("Checkout")
screen.set("address", "Ankara")
var model = ["address": "Ankara"]

let picker = AddressPicker()
let checkout = CheckoutScreen(screen)
let start = check("start", model: model, screen: screen, keys: ["address"])

model["address"] = "Izmir"
picker.choose("Izmir")
let unwired = check("no-delegate", model: model, screen: screen, keys: ["address"])

picker.delegate = checkout
picker.choose("Izmir")
let wired = check("delegate-wired", model: model, screen: screen, keys: ["address"])

countVerdicts(start + unwired + wired)
```

```
start | Checkout.address: model=Ankara screen=Ankara -> matching
no-delegate | Checkout.address: model=Izmir screen=Ankara -> stale
delegate-wired | Checkout.address: model=Izmir screen=Izmir -> matching
matching: 2
stale: 1
missing: 0
leftover: 0
```

In the `start` step it is matching. In the `no-delegate` step the model is `Izmir`, the screen is `Ankara` and the result is `stale`: the picker chose the address but passed it to no one. In the `delegate-wired` step the same call reached the screen this time and the row is matching. The count is two matching, one stale.

**LC1.** This measurement assumes that the delegate's method is called synchronously. Whether the call ran on another queue was not measured.

#### A Released Delegate

A delegate that was never wired is the first break that comes to mind. A more insidious one is a wired delegate that is released. The screen closes or another object stops holding it; the weak reference becomes `nil` and the picker keeps making calls as if it were wired. In this lesson becoming `nil` is not a guess but is actually measured: the reference is read twice below.

```swift
// main.swift — a weak reference becomes nil when the delegate is released, the call is dropped
let screen = Screen("Checkout")
screen.set("address", "Ankara")
let model = ["address": "Bursa"]

let picker = AddressPicker()
var handler: CheckoutScreen? = CheckoutScreen(screen)
picker.delegate = handler
print("delegate wired: \(picker.delegate != nil)")

handler = nil
print("delegate wired: \(picker.delegate != nil)")

picker.choose("Bursa")
let released = check("delegate-released", model: model, screen: screen, keys: ["address"])

countVerdicts(released)
```

```
delegate wired: true
delegate wired: false
delegate-released | Checkout.address: model=Bursa screen=Ankara -> stale
matching: 0
stale: 1
missing: 0
leftover: 0
```

The first line is `true`, the second line `false`. When the delegate was released, the weak reference became `nil` by itself; the following `choose` call went nowhere. The screen stayed `Ankara`, the model is `Bursa` and the result is `stale`. The count is one stale.

The first two lines are not an assumption of the model but the real result of Swift's weak reference behavior. The stale in the third line is the consequence of those two lines: since the call was dropped, nothing was written to the screen.

**LC2.** This measurement assumes only that setting the `handler` variable to `nil` is a release. The case where another strong reference keeps the object alive was not measured.

On the SOURCE side, the same link is built by a view controller making itself the delegate while setting up a picker.

```swift
import UIKit

final class CheckoutController: UIViewController, AddressPickerDelegate {
    let picker = AddressPicker()

    override func viewDidLoad() {
        super.viewDidLoad()
        picker.delegate = self
    }
}
```

If the `picker.delegate = self` line is forgotten, the compiler does not warn; only the screen stays stale.

#### Protocol Design

In the delegate pattern, an object asks another object or informs it at a point where a decision has to be made on its behalf. The asking object does not know who the **delegate** is; it only knows that it conforms to a **protocol**. In this way the owner of the question and the owner of the answer are separated. Most ready-made views work with this pattern: table views, picker controllers and text fields leave their behavior to their delegates.

A good delegate protocol has three properties. First, the method name describes the event with a verb pattern: something will happen (`will`), is happening (`did`) or needs approval (`should`). Second, the first parameter is the object that sends the event; in this way a single delegate can serve several objects and tell which one a call came from. Third, the protocol is small: each method carries one decision or notification, and the protocol itself does not describe the whole behavior of a class.

```swift
import UIKit

protocol AddressPickerDelegate: AnyObject {
    func addressPicker(_ picker: AddressPicker, didSelect address: String)
    func addressPickerDidCancel(_ picker: AddressPicker)
}
```

The protocol is restricted with `AnyObject`, so only classes can conform. The reason is in the next heading.

#### The Weak Reference

The object that holds a delegate stores it with a **weak reference**. The reason is the memory cycle: if the screen holds the picker and the picker holds the screen as its delegate, the two objects do not let go of each other and neither is ever released. A weak reference does not keep the object alive; if the delegate disappears elsewhere, the reference becomes `nil` by itself.

```swift
import UIKit

final class AddressPicker {
    weak var delegate: AddressPickerDelegate?

    func choose(_ address: String) {
        delegate?.addressPicker(self, didSelect: address)
    }
}
```

`weak` is valid only for class types; this is why restricting the protocol with `AnyObject` is necessary. If the delegate is `nil`, the call is skipped silently, and this is the real trap of this lesson. If the delegate was not wired, the picker works, gives no error and nothing happens.

#### A Delegate That Is Not Wired

Never assigning the delegate is the most common mistake of the pattern. Because the call is made with optional chaining, there is no crash and no warning; the screen does not respond. The first and second measurements of this lesson show this. A selection made before the delegate is assigned does not reach the screen and the row stays stale. Once assigned, the same selection gives a matching result. The second measurement also really tests the weak reference: when the screen is released, the picker's link to the delegate becomes `nil` and later selections again reach no one.

#### Delegate or Closure

The same job can often be done with a closure as well. The picker can take as a property a closure that it will call when the selection is complete. For a single event this is shorter and does not require writing a protocol. If there are several related events, for example selection, cancellation and a validation question, a protocol is more readable, and because the sending object is passed as a parameter, a single delegate can serve several objects.

Closures can also create a memory cycle: if the closure holds the screen and the screen holds the picker, a cycle is formed and the weak reference has to be written separately inside the closure. In the delegate pattern this risk is collected in a single place, in the `weak` keyword.

#### Common Mistakes

- Holding the delegate with a strong reference. A cycle forms and neither object is ever released.
- Forgetting to assign the delegate. The call is skipped silently; there is no output.
- Not restricting the protocol with `AnyObject`. A weak reference cannot be written and the compiler gives an error.
- Not passing the sending object as a parameter. When a single delegate serves several objects, it is not known which one called.
- Doing heavy work in the delegate. The call runs in the picker's flow, and long work blocks the screen.
- Gathering the protocol used to ask the delegate for data and the protocol used for notification in one place. The first expects an answer, the second does not; this distinction is kept in the naming.

#### Interpreting the Measured Result

In the first measurement the screen agreed with the model at the start. When the model became `Izmir`, the screen stayed `Ankara` because the delegate was not wired, and the row came out stale. When the delegate was wired, the same selection reached the screen and the row became matching. In the second measurement the weak reference was really measured: the link printed `true` first and `false` once the screen was released, and since the following selection reached no one, the row came out stale. In the third measurement two different screens reached the same result: one stayed stale because the delegate was not wired, the other because the delegate was wired but did not write the selection to the screen. The output does not tell the two apart.

The conclusion is this: the delegate pattern can fail without producing an error. A broken link and a wired delegate that does not do its job give the same silent picture, and the only way to see it is to compare the output with the model.

In practice two checks are enough. The first is that the place where the delegate is assigned is single and explicit: while the screen is being set up, the picker is created and the delegate is assigned immediately, so the assignment is not left in between. The second is not leaving a silent path behind the delegate call. If the result of the call matters, a condition that warns when there is no delegate is added, or it is stopped with a check during development. Silent skipping is the comfortable side of the pattern, but as the measurement shows, it also hides an error. These two habits prevent most of the stale rows seen in the measurement at the writing stage already; when the place where the link is built is single, a link that breaks or is never built is also looked for in a single place and is not tracked separately on every screen.

#### The Bounding Measurement

A stale result can have two separate causes: the delegate may never have been wired, or it is wired and the method writes nothing. One is forgetting the link, the other is leaving the inside of the method empty. **The rule does not read who forgot.** The model looks only at what was written to the screen.

```swift
// main.swift — not being wired and a method that writes nothing give the same stale result
let model = ["address": "Bursa"]
let unwiredScreen = Screen("UnwiredCheckout")
let silentScreen = Screen("SilentCheckout")
unwiredScreen.set("address", "Ankara")
silentScreen.set("address", "Ankara")

let unwiredPicker = AddressPicker()
unwiredPicker.choose("Bursa")

let silentPicker = AddressPicker()
let silentDelegate = SilentCheckout()
silentPicker.delegate = silentDelegate
silentPicker.choose("Bursa")

let unwiredResult = check("not-wired", model: model, screen: unwiredScreen, keys: ["address"])
let silentResult = check("method-wrote-nothing", model: model, screen: silentScreen, keys: ["address"])

countVerdicts(unwiredResult + silentResult)
```

```
not-wired | UnwiredCheckout.address: model=Bursa screen=Ankara -> stale
method-wrote-nothing | SilentCheckout.address: model=Bursa screen=Ankara -> stale
matching: 0
stale: 2
missing: 0
leftover: 0
```

Both rows are `stale`, the count is two stale. One was never called, the other was called and wrote nothing; the output does not tell them apart. This is a limit: to fix the result, where the call broke has to be traced separately, and the model does not say that.

**LC3.** This measurement compares the two causes with the same model value. The correctness of the argument passed when the delegate's method is called was not measured separately.

#### Summary

- A delegate is the pattern in which an object hands part of its work to another object that conforms to a protocol.
- The delegate is held weakly so that no retain cycle forms; when the delegate is released, the weak reference becomes `nil` and the call is dropped.
- A delegate that is not wired or has been released leaves the screen stale; once wired, the screen is matching.
- The weak reference becoming `nil` was actually measured in this lesson.
- Not being wired and a method that writes nothing give the same stale result; the rule does not read who forgot.
- This lesson's agreement: **stale**.

#### Next Step

The delegate builds the link between objects; drawing determines what a view writes to its own surface. In the next lesson it will be measured that a view drawn with `draw(_:)` keeps showing the old value when no redraw is requested.

### Drawing and Layer Animation

Source: https://academia.sh/en/courses/uikit/drawing-and-layer-animation

A measurement of how a ring drawn with draw(_:) stays stale when no redraw is requested, how a value returns to its old state when the model layer is not written in a layer animation, and how the duration is not measured.

In the previous lesson, The Delegate Pattern, we measured that a broken link silently leaves the screen stale. In this lesson the break concerns a view's own surface. Some views are not built from ready-made components; they **draw** their own content themselves. The drawing code runs only when it is called, and calling it requires a **redraw request**. Even if the model changes, the drawing stays old unless a request arrives.

The measurement uses the core file.

```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)")
    }
}
```

The core compares every field shown on screen with the model's current value. In this lesson the value on the screen is the value the drawing last drew. The model builds what the code writes to the screen; it is not the framework itself.

#### The Drawing Stays Old Unless It Is Requested Again

Consider a progress ring on a loading screen. The filled arc of the ring is drawn according to a percentage value. The view does its drawing by overriding the `draw(_:)` method; the method is called when the view needs to be redrawn.

```swift
import UIKit

final class ProgressRingView: UIView {
    var progress: Int = 0

    override func draw(_ rect: CGRect) {
        let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY),
                                radius: rect.width / 2,
                                startAngle: -.pi / 2,
                                endAngle: -.pi / 2 + 2 * .pi * CGFloat(progress) / 100,
                                clockwise: true)
        path.stroke()
    }
}
```

`progress` is a number and `draw(_:)` reads it. But when `progress` changes, `draw(_:)` is not called by itself. For the view to be redrawn, `setNeedsDisplay()` has to be called; this call leaves a **redraw request** with the framework and the drawing runs in the next pass. The measurement has three steps: the ring is drawn, the model changes the percentage, and the request is made.

```swift
// main.swift — the ring stays at the old value unless a redraw is requested
let ring = Screen("ProgressRing")
var model = ["progress": "40"]

ring.set("progress", "40")
let drawn = check("first-draw", model: model, screen: ring, keys: ["progress"])

model["progress"] = "70"
let stale = check("model-changed", model: model, screen: ring, keys: ["progress"])

ring.set("progress", "70")
let redrawn = check("redraw-requested", model: model, screen: ring, keys: ["progress"])

countVerdicts(drawn + stale + redrawn)
```

```
first-draw | ProgressRing.progress: model=40 screen=40 -> matching
model-changed | ProgressRing.progress: model=70 screen=40 -> stale
redraw-requested | ProgressRing.progress: model=70 screen=70 -> matching
matching: 2
stale: 1
missing: 0
leftover: 0
```

In the `first-draw` step it is matching. In the `model-changed` step the model is `70`, the screen is `40` and the result is `stale`: the ring keeps drawing the old percentage. In the `redraw-requested` step the drawing ran again and the row is matching. The count is two matching, one stale.

On the SOURCE side, the request can also be made inside the value itself. An observer is added to the property and a request is left every time the value changes.

```swift
import UIKit

extension ProgressRingView {
    func update(to percent: Int) {
        progress = percent
        setNeedsDisplay()
    }
}
```

In this form every path that calls `update(to:)` also leaves the request. What is measured here is not the drawing itself but whether the request was written.

**LC1.** This measurement represents the redraw request with the step of writing to the screen. Whether the request ran in the framework's next pass or whether two requests merged was not measured.

#### Model Layer and Presentation Layer

Under a view there is a **layer**. Layer animations let a value move from the old to the new through intermediate values. Two layers appear here: the **model layer** carries the final state of the value at once, while the **presentation layer** shows the intermediate value visible on screen at that moment. If an animation is added but the value of the model layer is not written, the layer returns to its old value when the animation ends.

```swift
import UIKit

func fadeIn(_ ring: UIView) {
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 0.4
    ring.layer.add(animation, forKey: "fade")
}

func fadeInAndCommit(_ ring: UIView) {
    ring.layer.opacity = 1
    fadeIn(ring)
}
```

`fadeIn` only adds the animation; the `opacity` in the model layer stays old. `fadeInAndCommit` first writes the final value to the model layer and then adds the animation. Here the model builds only the model layer: the intermediate value visible on screen in the middle of the animation is not measured. We measure the two steps.

```swift
// main.swift — if an animation is added and the model layer is not written, the value returns to its old state
let layer = Screen("RingLayer")
let model = ["ringOpacity": "1"]

layer.set("ringOpacity", "0")
let added = check("animation-added", model: model, screen: layer, keys: ["ringOpacity"])

layer.set("ringOpacity", "1")
let committed = check("model-layer-written", model: model, screen: layer, keys: ["ringOpacity"])

countVerdicts(added + committed)
```

```
animation-added | RingLayer.ringOpacity: model=1 screen=0 -> stale
model-layer-written | RingLayer.ringOpacity: model=1 screen=1 -> matching
matching: 1
stale: 1
missing: 0
leftover: 0
```

In the first step the model is `1`, the screen is `0` and the result is `stale`: the animation was added but the value of the layer was not written. In the second step, once the model layer is written, the row is matching. The count is one matching, one stale. The intermediate value that the presentation layer shows in the middle of the animation is in neither of these two rows; the output compares only the value that stays in the layer after the animation ends.

**LC2.** This measurement represents the model layer as a single value. The intermediate values of the presentation layer, the moment the animation completes and the cancellation of the animation were not measured.

#### The Framework Calls the Drawing

In a hand-drawn view your code never calls the `draw(_:)` method directly. The caller is the framework, and it does so when the view is marked as needing drawing, while the screen prepares the next pass. This marking is done with `setNeedsDisplay()`. The call does not draw at once; it only leaves a request. If several requests arrive in the same pass, the framework merges them and does the drawing once. For this reason, calling `setNeedsDisplay()` a hundred times in a loop does not mean a hundred drawings.

It is also possible to narrow the scope of the request. `setNeedsDisplay(_:)` takes a rectangle and asks for only that region to be redrawn; the `rect` parameter that the `draw(_:)` method receives carries that region. In a large view where only a small part changed, the drawing cost drops this way.

#### Layout and Drawing Are Separate Requests

`setNeedsDisplay()` is for drawing; a change of size or position is requested with `setNeedsLayout()`. Mixing the two is a common mistake. When the percentage of the ring changes, the drawing has to be refreshed and the size does not change; so the right call is `setNeedsDisplay()`. When the constant of a constraint changes, layout is needed, not drawing. Whether drawing is needed when the bounds of the view change is decided by `contentMode`: the default value stretches the view, while the `redraw` value asks for the drawing again on every bounds change.

```swift
import UIKit

final class RingContainer: UIView {
    var progress: Int = 0 {
        didSet { setNeedsDisplay() }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // The size changed, the layout was updated; drawing is a separate request.
    }
}
```

The `didSet` observer leaves the request by itself every time the value changes. In this way forgetting the request becomes impossible. If a separate update method is written instead of an observer, every path that does not call that method leaves the screen stale.

#### Two Trees in a Layer Animation

The layer under a view works with two separate layer trees. The model layer carries the value the code wrote. The presentation layer carries the value visible on screen at that moment and walks through the intermediate values during an animation. The presentation layer is reached with the `presentation()` method and its values can be read, but not written.

When an animation is added, the model layer does not change. The animation drives only the presentation layer; when it ends, the presentation layer returns to the value of the model layer. For this reason the value you want to remain at the end of the animation must first be written to the model layer. There are also the `isRemovedOnCompletion` and `fillMode` settings for holding the animation in its end state, but these hide the distinction between the model and the presentation and give a wrong value on the next read. The right way is to write the model layer first and start the animation from the starting value.

#### Drawing or a Ready-Made Component

Hand drawing is not needed on every screen. Ready-made components such as labels, images and buttons draw text, pictures and touch themselves. The `draw(_:)` method is used for forms that a ready-made component does not cover, such as a progress ring, a chart or a custom badge. If the drawing path is fixed and the value is only a number, a solution made of layer objects is also possible: a shape layer builds the path once, and the percentage is updated by changing only a property. This is cheaper than redrawing the whole view on every change and combines naturally with layer animation.

The effect of the choice on the measuring axis is this: in hand drawing, the value of the screen is the value at the moment the drawing last ran, and leaving the request counts as a writing step. In the layer solution, the value of the screen is directly in the layer.

#### Common Mistakes

- Calling the `draw(_:)` method directly. There is no valid drawing context and the result is not drawn.
- Asking for layout instead of `setNeedsDisplay()`. Layout does not refresh the drawing.
- Adding an animation and not writing the model layer. When the animation ends, the layer returns to its old value; this is the stale row in the second measurement of this lesson.
- Writing to the presentation layer. The values are erased in the next pass.
- Creating new objects inside `draw(_:)`. Memory allocated on every drawing breaks fluidity; the drawing path and colors are prepared outside.
- Changing state inside drawing. Drawing only reads; if the call that writes the state requests the drawing again, a loop forms.

#### Interpreting the Measured Result

In the first measurement the ring was drawn when the percentage was `40` and it was matching. When the model became `70`, the screen stayed `40` and the row became stale; when the drawing request was left, the row returned to matching. The difference between them is a single call, but in the output only the last value of the screen is visible. In the second measurement the same structure repeated in the layer value: the animation was added, the model layer was not written and the result was stale. The third measurement showed that the duration is not in the model; since two layers with different durations reached the same final value, both came out matching. The two measurements complement each other. In drawing, what has to be written is a request; in animation, what has to be written is a value; if neither is written, the screen falls behind the model.

In practice two checks are enough. For every hand-drawn value, one makes sure that every path that writes the value also leaves the drawing request; the safest form is to attach an observer to the value itself. For every animated value, one verifies that the final value is written to the model layer before the animation is added. Both can be seen by looking at the code, but on the screen they appear only during testing, after a particular change. The measurement turns these two checks into one line each. This approach makes the two common causes of the screen falling behind the model catchable in code review; because what is looked for is a single line in both cases, the absence of that line is noticed before it shows on the screen. Moreover, these checks do not grow with the number of screens; every new screen is reviewed with the same two questions, and the answer is given each time by looking at the code.

#### The Bounding Measurement

Drawing and animation are about time, but the measure of this lesson does not cover time. **The model layer carries the final value at once, the presentation layer shows an intermediate value; the model builds only the model layer.** The duration of the animation is not a field in the model.

Suppose we write the same final value with two animations, a fast one and a slow one. In the SOURCE fragment the duration was a parameter; it has no counterpart in the model, so the two screens give the same result.

```swift
// main.swift — the animation duration is not in the model, two layers give the same result
let model = ["ringOpacity": "1"]
let fast = Screen("FastRing")
let slow = Screen("SlowRing")

fast.set("ringOpacity", "1")
slow.set("ringOpacity", "1")

let fastResult = check("short-duration", model: model, screen: fast, keys: ["ringOpacity"])
let slowResult = check("long-duration", model: model, screen: slow, keys: ["ringOpacity"])

countVerdicts(fastResult + slowResult)
```

```
short-duration | FastRing.ringOpacity: model=1 screen=1 -> matching
long-duration | SlowRing.ringOpacity: model=1 screen=1 -> matching
matching: 2
stale: 0
missing: 0
leftover: 0
```

Both rows are matching and the count is two matching. The difference in duration did not enter the output. This is a limit: the model says which value was written to the screen; it does not say how long it took to reach that value.

**LC3.** This measurement tests that two layers carry the same final value. No number was written for the frame time or the animation curve.

#### Summary

- A hand-drawn view refreshes its drawing only when a redraw request arrives; if the request is not written, the screen stays stale.
- Once the request is written, the drawing runs again and the screen is matching.
- In a layer animation the model layer carries the final value at once and the presentation layer shows an intermediate value.
- If an animation is added and the model layer is not written, the value returns to its old state and the screen stays stale.
- The duration of the animation is not in the model; the same final value gives the same agreement with both durations.
- This lesson's agreement: **stale**.

#### Next Step

In this topic we built the interface imperatively: constraints, cells and drawing were managed with code. The next topic, bridging, will bring the imperative and the declarative interface together on the same screen. The hosting controller and wrapping will let the two sides pass values to each other; there too we will measure the screen's agreement with the model using the same core.

## Bridging

Declarative and imperative interfaces wrapping each other to run in the same app, incremental screen-by-screen migration of legacy code, and which interface framework to choose in which situation.

### Declarative and Imperative Interfaces Together

Source: https://academia.sh/en/courses/uikit/declarative-and-imperative-together

Measuring how a screen falls behind the model when the root view is not reassigned while embedding a declarative view in an imperative screen, and when no update is written while wrapping an imperative view in a declarative screen.

In the previous two topics we read the imperative interface on its own: every value we saw on screen was written by code, and nothing reminded the code that forgot to write. In the previous course (M30/K03) we saw the opposite on the declarative side: when a value changed, the framework recomputed the view that reads it by itself. Real applications are rarely built from only one of the two. An old screen carries a declarative piece, a new screen takes an old piece inside it. In this lesson we look at the seam between the two.

The seam has two directions. A declarative view can enter an imperative screen, which we will call **embedding**. An imperative view can enter a declarative screen, which we will call **wrapping**. In embedding, the declarative framework takes the input and the framework computes it; in wrapping, the imperative view takes the input and code writes it. Two frameworks standing side by side on the same screen can exchange values only through these two paths. The frameworks are called UIKit and SwiftUI, but the lesson will not depend on names: we will say the declarative framework and the imperative interface framework.

The measuring axis has been the same since the start of the topic: **agreement**. The question is whether the field shown on screen agrees with the model's current value. For each field the rule returns one of four results: `matching`, `stale`, `missing`, `leftover`. In bridging the question turns into this: when a value crosses the border between the two frameworks, who carries it?

The core file is the same.

```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)")
    }
}
```

The rule does not see the cause, it sees only two values. Telling whether a value stayed missing, stale or leftover on screen does not require knowing why; that is why the same rule can be applied to the screens of both frameworks. `Screen` holds the values the screen shows; every call that writes to the screen writes it with `set`, and the model writes in the same way.

#### A Declarative View Embedded in an Imperative Screen

Think of a task screen. At the top there is a summary strip: a title, an open task count, a filter. The strip is written in the declarative framework, the rest of the screen stands in the imperative framework. A declarative view enters an imperative screen through a **hosting controller**: a class that wraps the view and adds it as a child controller. The root view is the controller's `rootView` property.

```swift
import UIKit
import SwiftUI

final class HostingController: UIViewController {
    private var host: UIHostingController<SummaryView>!
    var summary = SummaryState(title: "Tasks", openCount: 3, filter: "open")

    override func viewDidLoad() {
        super.viewDidLoad()
        host = UIHostingController(rootView: SummaryView(state: summary))
        addChild(host)
        view.addSubview(host.view)
        host.didMove(toParent: self)
    }

    func summaryDidChange() {
        // host.rootView = SummaryView(state: summary)
    }
}
```

The place to watch is `summaryDidChange`. In the declarative framework, when a view's input changes, the framework recomputes the view. But here the input is a **copy** handed to the constructor of `SummaryView`: even if the `summary` property changes later, the state in the root view's hands stays as it was. The framework's recomputation kicks in only when `rootView` is reassigned. Without the assignment line the framework does not see the change; that is the imperative side's job.

The model below builds what the code writes to the screen; it is not the framework itself. The hosting controller is rebuilt in plain Swift and carries the names in the SOURCE snippet: `HostingController`, `rootView`, `SummaryView`, `SummaryState`. The shared helper file builds both screens.

```swift
// Bridging.swift — plain Swift setup of two screens, which assignment writes what
struct SummaryState {
    var title: String
    var openCount: Int
    var filter: String

    var values: [String: String] {
        ["title": title, "openCount": String(openCount), "filter": filter]
    }
}

struct SummaryView {
    let state: SummaryState

    func draw(on screen: Screen) {
        screen.set("title", state.title)
        screen.set("openCount", String(state.openCount))
        screen.set("filter", state.filter)
    }
}

final class HostingController {
    let screen = Screen("HostingController")
    var rootView: SummaryView {
        didSet { rootView.draw(on: screen) }
    }

    init(rootView: SummaryView) {
        self.rootView = rootView
        rootView.draw(on: screen)
    }
}

final class LegacyListView {
    let screen = Screen("LegacyListView")

    init(title: String, rows: [String]) {
        screen.set("title", title)
        reloadData(rows: rows)
    }

    func reloadData(rows: [String]) {
        screen.set("rowCount", String(rows.count))
    }
}
```

The first event: the screen is built with three open tasks, then the model goes up to five and `rootView` is not reassigned.

```swift
// main.swift — model changed, rootView not reassigned
let first = SummaryState(title: "Tasks", openCount: 3, filter: "open")
let latest = SummaryState(title: "Tasks", openCount: 5, filter: "open")

let hosting = HostingController(rootView: SummaryView(state: first))
let withoutAssignment = check("hosting-unassigned", model: latest.values, screen: hosting.screen,
                              keys: ["title", "openCount", "filter"])

countVerdicts(withoutAssignment)
```

```
hosting-unassigned | HostingController.title: model=Tasks screen=Tasks -> matching
hosting-unassigned | HostingController.openCount: model=5 screen=3 -> stale
hosting-unassigned | HostingController.filter: model=open screen=open -> matching
matching: 2
stale: 1
missing: 0
leftover: 0
```

Two of the three fields are matching, because they did not change in the model. `openCount` is 5 in the model and 3 on screen: **stale**. No error shows on screen, the strip is drawn nicely; only the number is old. The compiler does not warn either, because there is no wrongly written line, there is an unwritten one. When such errors are found by eye, it is usually by someone opening the screen a second time; the measurement runs the same check for every field from start to end.

**BR1.** The model assumes that, when the `rootView` assignment is made, the declarative view writes its new state to the screen. How and at what moment the declarative framework recomputes the view after the assignment is not measured; `draw(on:)` writes the result of that computation in a single call. This is an assumption and holds only for values crossing this border.

Now we open the line and apply the same change once more, this time with the assignment. In this event only one thing changes: `rootView` is reassigned with the new state. The starting values, the model and the checked fields are the same as before, so the difference between the two outputs ties to a single line.

```swift
// main.swift — model changed, rootView reassigned
let first = SummaryState(title: "Tasks", openCount: 3, filter: "open")
let latest = SummaryState(title: "Tasks", openCount: 5, filter: "open")

let hosting = HostingController(rootView: SummaryView(state: first))
hosting.rootView = SummaryView(state: latest)
let withAssignment = check("hosting-assigned", model: latest.values, screen: hosting.screen,
                           keys: ["title", "openCount", "filter"])

countVerdicts(withAssignment)
```

```
hosting-assigned | HostingController.title: model=Tasks screen=Tasks -> matching
hosting-assigned | HostingController.openCount: model=5 screen=5 -> matching
hosting-assigned | HostingController.filter: model=open screen=open -> matching
matching: 3
stale: 0
missing: 0
leftover: 0
```

All three fields are matching. The fix is a single line, but remembering to write that line is left to the programmer. In the declarative framework the framework did this work; the hosting controller hands that work back to the imperative side. The solution lies in the same place: the code that sees the change, that is `summaryDidChange`, must reassign the root view. For a state that can change from several places, gathering this call in a single point reduces the number of places where the assignment can be forgotten.

#### An Imperative View Wrapped in a Declarative Screen

The seam works in the opposite direction too. An old list screen has been running soundly for years and a new declarative screen wants to take it inside as it is. What brings an imperative view into the declarative tree is **wrapping**: a type with two required methods, one builds the view once, the other updates the imperative view as the declarative state changes.

```swift
import SwiftUI

struct ListRepresentable: UIViewRepresentable {
    let title: String
    let rows: [String]

    func makeUIView(context: Context) -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView, context: Context) {
        // list.reloadData(rows: rows)
    }
}
```

`makeUIView` runs once while the screen opens. `updateUIView(_:context:)` is called each time the declarative state changes; carrying the new value to the imperative view is its job. Here its body is left empty: the model sets up the situation where nothing is written when the framework makes the call.

**BR2.** The model assumes that the framework calls the `updateUIView` method when the state changes; in the measurement our code makes the call. The timing and frequency of the call are not measured. What is measured is what the body writes to the imperative screen when the call is made.

```swift
// main.swift — updateUIView body empty, list is not reloaded
struct ListRepresentable {
    let title: String
    let rows: [String]

    func makeUIView() -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView) {
        // empty
    }
}

let before = ListRepresentable(title: "Tasks", rows: ["a", "b", "c"])
let after = ListRepresentable(title: "Tasks", rows: ["a", "b", "c", "d", "e"])

let list = before.makeUIView()
after.updateUIView(list)

let model = ["title": "Tasks", "rowCount": String(after.rows.count)]
let emptyUpdate = check("wrapping-empty", model: model, screen: list.screen,
                        keys: ["title", "rowCount"])

countVerdicts(emptyUpdate)
```

```
wrapping-empty | LegacyListView.title: model=Tasks screen=Tasks -> matching
wrapping-empty | LegacyListView.rowCount: model=5 screen=3 -> stale
matching: 1
stale: 1
missing: 0
leftover: 0
```

The title was written at the first setup and did not change: matching. The row count is 5 in the model, while the list stayed with its first three rows: stale. This has the same shape as the first event; we are only crossing the border from the opposite direction. In both cases the call that would carry the value has not been written. In embedding, what was missing was the assignment; in wrapping, what is missing is the `reloadData` call; the rule names both with the same word, `stale`.

Now we write the body. The snippet carries the `rows` value from the declarative state to the imperative view with `reloadData`; everything else is the same as the previous snippet.

```swift
import SwiftUI

struct ListRepresentable: UIViewRepresentable {
    let title: String
    let rows: [String]

    func makeUIView(context: Context) -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView, context: Context) {
        list.reloadData(rows: rows)
    }
}
```

We measure the same two situations, this time with the `reloadData` call.

```swift
// main.swift — updateUIView body reloads the list
struct ListRepresentable {
    let title: String
    let rows: [String]

    func makeUIView() -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView) {
        list.reloadData(rows: rows)
    }
}

let before = ListRepresentable(title: "Tasks", rows: ["a", "b", "c"])
let after = ListRepresentable(title: "Tasks", rows: ["a", "b", "c", "d", "e"])

let list = before.makeUIView()
after.updateUIView(list)

let model = ["title": "Tasks", "rowCount": String(after.rows.count)]
let forwardedUpdate = check("wrapping-filled", model: model, screen: list.screen,
                            keys: ["title", "rowCount"])

countVerdicts(forwardedUpdate)
```

```
wrapping-filled | LegacyListView.title: model=Tasks screen=Tasks -> matching
wrapping-filled | LegacyListView.rowCount: model=5 screen=5 -> matching
matching: 2
stale: 0
missing: 0
leftover: 0
```

Both fields are matching. This is what wrapping is for: the path by which the declarative state reaches the imperative view after the view is built is `updateUIView`, and it carries as much as the lines written. The old list screen's own lifecycle is unaware of this call; the declarative framework announces the change, but the code that turns the announcement into something the imperative view understands is you.

#### The Bounding Measurement

In this section we separate the scope of the measurement. So far both screens showed only the fields that code wrote. A state the declarative view keeps **inside itself** is different: a collapsible section of the strip, a scroll position, a focus. The framework recomputes these values by itself and the screen code does not write them to `Screen`.

```swift
// main.swift — the declarative side's own state is never written to the screen
let state = SummaryState(title: "Tasks", openCount: 5, filter: "open")
let hosting = HostingController(rootView: SummaryView(state: state))

let model = ["openCount": "5"]
let ownState = check("own-state", model: model, screen: hosting.screen,
                     keys: ["openCount", "isExpanded"])

countVerdicts(ownState)
```

```
own-state | HostingController.openCount: model=5 screen=5 -> matching
own-state | HostingController.isExpanded: model=- screen=- -> out-of-scope
matching: 1
stale: 0
missing: 0
leftover: 0
```

`isExpanded` is absent on both sides: the result is `out-of-scope` and it does not enter the count. Whether this field is right on screen the rule does not say, because the model never set it up. A full matching count does not mean everything on the screen is up to date; it says only that the fields that were written are up to date. The declarative side's own recomputation stays outside this measurement. That is why the two assumptions above were written out openly: the model counts the work the declarative side does as written once at the assignment and call points. Telling when the real framework does this work is the job of the lesson text and the framework documentation, not of the measurement. This lesson claims only one thing: if the code itself did not write the call that carries the value across the border, the screen falls behind the model.

#### Summary

- A declarative view enters an imperative screen through a hosting controller; if `rootView` is not reassigned, the screen stays at three while the model goes up to five and the open task count is measured **stale** (2 matching, 1 stale).
- When the assignment is written, all three of the same fields are matching (3 matching).
- An imperative view enters a declarative screen by wrapping; if the `updateUIView` body is empty, the row count stays 3 on screen while it is 5 in the model (1 matching, 1 stale). When `reloadData` is written into the body, both fields are matching (2 matching).
- In both directions the imperative side writes the call that carries the value across the border; without the call the rule sees only the divergence and does not say its cause.
- When the declarative side's own state is not set up in the model, the result is `out-of-scope` and it does not enter the count (1 matching, 0 others).
- This lesson's agreement: **stale**.

#### Next Step

In the next lesson, embedding and wrapping do not stay on a single screen but spread across a whole application: we will cover the migration of legacy code to the declarative framework, screen by screen. There the direction of the question will change: we will look not at the value crossing the border, but at what happens when it does not find its place on the new screen.

### Incremental Migration of Legacy Code

Source: https://academia.sh/en/courses/uikit/incremental-migration

Measuring that a field not carried over stays missing on the new screen while moving a legacy screen to the new framework one screen at a time, that a field dropped from the model stays leftover on the old screen, and that the rule works per field, not per screen.

In the previous lesson (Declarative and Imperative Interfaces Together) we looked at the seam between the two frameworks: if no assignment or update call was written while a value crossed the border, the field stayed stale. In a real application there is a second question beside that seam. Legacy code contains hundreds of screens and no team rewrites them all overnight. The conversion moves screen by screen: at each step one screen moves to the declarative framework and the rest keep working as they were. The application must stay shippable even in the middle of the conversion.

This means the two frameworks live side by side for a long time. The reasoning for the incremental path is simple: because each step is small, the source of a bug is known when one is found. When a screen is moved, the only thing that can break is that screen. In a one-time rewrite the same bug can come from anywhere in the application, and nobody knows when it got in. The order of moving usually runs from the leaves toward the root: a detail screen with few fields, which opens no other screen, is the first candidate, because it is small and easy to roll back if it fails. At the seam, both options from the previous lesson apply: a new screen is pushed onto an old navigation stack through a hosting controller, or an old piece enters a new screen through wrapping.

What we will measure in this lesson is which of the things the old screen did pass to the new screen when a screen is moved. The measuring axis has not changed: **agreement**. For each field one of four results comes out: `matching`, `stale`, `missing`, `leftover`. The core file is the same as in the previous lessons.

```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)")
    }
}
```

The things an old screen does are often not written down anywhere. The person who wrote the old screen has left, there is no documentation, the screen has grown over the years through small additions. The person writing the new screen reads the old one and rebuilds it, and may overlook a field while reading. The rule catches this oversight without caring who made it, by saying that the field is in the model but not on the screen. That is why the finish line of a conversion step is not "the screen opens"; it should be "every field of the model finds a counterpart on the screen". The first criterion is met by eye, the second by comparing field by field.

#### A Field That Was Not Carried Over

We have a task screen. The old version is written in the imperative interface framework and shows three values: a title, an open task count and a due label. Each value is written to a label through an **outlet**; the code that writes runs in a method just before the screen appears. There is also an old badge on the screen.

```swift
import UIKit

final class LegacyTaskController: UIViewController {
    @IBOutlet private var titleLabel: UILabel!
    @IBOutlet private var countLabel: UILabel!
    @IBOutlet private var dueLabel: UILabel!
    @IBOutlet private var badgeLabel: UILabel!

    func bind(_ task: TaskModel) {
        titleLabel.text = task.title
        countLabel.text = String(task.openCount)
        dueLabel.text = task.dueLabel
        badgeLabel.text = "new"
    }
}
```

In the first step of the conversion the screen is rewritten in the declarative framework. The new view takes the title and the count and shows them. The due label was skipped during the rewrite.

```swift
import SwiftUI

struct TaskScreenView: View {
    let task: TaskModel

    var body: some View {
        VStack {
            Text(task.title)
            Text(String(task.openCount))
        }
    }
}
```

Comparing the two pieces, the difference can be seen, but on most real screens there are twelve fields and the difference goes unnoticed. The model builds both screens in plain Swift: who writes what to the screen. The shared types stay in a helper file; the names `TaskModel`, `LegacyTaskController`, `bind` and `body` in the SOURCE snippets are the same here. If `Label` is dropped from the end of the label names, the keys come out: `title` for `titleLabel`, `dueLabel` for `dueLabel`.

```swift
// Migration.swift — the model's value and the setup of the old screen
struct TaskModel {
    var title: String
    var openCount: Int
    var dueLabel: String

    var values: [String: String] {
        ["title": title, "openCount": String(openCount), "dueLabel": dueLabel]
    }
}

final class LegacyTaskController {
    let screen = Screen("LegacyTaskController")

    func bind(_ task: TaskModel) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
        screen.set("dueLabel", task.dueLabel)
        screen.set("badge", "new")
    }
}
```

The model builds what the code writes to the screen; it is not the framework itself. First event: the new screen is built without writing `dueLabel`, and the model asks for all three fields.

```swift
// main.swift — the new screen does not carry the dueLabel field
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
    }
}

let task = TaskModel(title: "Tasks", openCount: 5, dueLabel: "tomorrow")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let dropped = check("new-screen-missing", model: task.values, screen: migrated,
                    keys: ["title", "openCount", "dueLabel"])

countVerdicts(dropped)
```

```
new-screen-missing | TaskScreenView.title: model=Tasks screen=Tasks -> matching
new-screen-missing | TaskScreenView.openCount: model=5 screen=5 -> matching
new-screen-missing | TaskScreenView.dueLabel: model=tomorrow screen=- -> missing
matching: 2
stale: 0
missing: 1
leftover: 0
```

Two fields were carried over and are matching. `dueLabel` is `tomorrow` in the model and does not exist on the new screen: **missing**. This is different from `stale`. With stale there was an old value on screen; here the screen never wrote the field at all. The root cause comes from the same place as the bug in the previous lesson, an unwritten line, but the outcome is named separately because it is noticed differently. A stale number can be misread, while a missing label shows up on the screen as a blank.

**BR1.** The model assumes that every value the new screen writes in `body` has been written to the screen. When the declarative framework calls `body` and what it recomputes on a change are not measured; what is measured is which fields `body` supplies.

Fixing the missing field is short: add `Text(task.dueLabel)` to the new view. In the measurement we do this by writing one line in `body`.

```swift
// main.swift — dueLabel carried over to the new screen
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
        screen.set("dueLabel", task.dueLabel)
    }
}

let task = TaskModel(title: "Tasks", openCount: 5, dueLabel: "tomorrow")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let carried = check("new-screen-carried", model: task.values, screen: migrated,
                    keys: ["title", "openCount", "dueLabel"])

countVerdicts(carried)
```

```
new-screen-carried | TaskScreenView.title: model=Tasks screen=Tasks -> matching
new-screen-carried | TaskScreenView.openCount: model=5 screen=5 -> matching
new-screen-carried | TaskScreenView.dueLabel: model=tomorrow screen=tomorrow -> matching
matching: 3
stale: 0
missing: 0
leftover: 0
```

All three fields are matching. The conversion is complete for this field. One more point to note: this fix added one line to `body` and changed nothing else. This is the useful side of incremental conversion. The place of the bug is known, the fix is small, and after the fix the measurement shows with the same counts that the bug is gone.

#### A Field Dropped From the Model

The flip side of conversion is a field the old screen still writes but that no longer corresponds to anything. Remember the badge: the old screen has the line `badgeLabel.text = "new"` and it runs unconditionally. Suppose the badge was removed by a product decision. The model no longer carries a `badge` field, but the old screen keeps writing it.

```swift
// main.swift — the model does not carry the badge field, the old screen keeps writing it
let task = TaskModel(title: "Tasks", openCount: 5, dueLabel: "tomorrow")
let legacy = LegacyTaskController()
legacy.bind(task)

let leftover = check("old-screen-leftover", model: task.values, screen: legacy.screen,
                     keys: ["title", "badge"])

countVerdicts(leftover)
```

```
old-screen-leftover | LegacyTaskController.title: model=Tasks screen=Tasks -> matching
old-screen-leftover | LegacyTaskController.badge: model=- screen=new -> leftover
matching: 1
stale: 0
missing: 0
leftover: 1
```

`title` is matching. For `badge` there is no model value (`-`) and the screen says `new`: **leftover**. Once more the rule does not see the cause; it cannot tell whether the field was removed or the old screen writes it by mistake. It says only that there is a value on the screen that the model does not know. Old code has many lines of this kind: the remnant of a removed feature, a constant text put in for debugging, a label nobody reads any more, and these are also the easiest to disappear when moving to a new screen; but when a field is not carried over too, missing and leftover give two different answers to the same question: what is in the model is not on the screen, what is on the screen is not in the model.

A leftover has two fixes and the measurement does not say which is right. If the badge was really removed, the line on the old screen is deleted and the field drops out of the result. If the badge was not removed, the model gives it a value again and the result becomes matching. The first fix changes the old screen, the second changes the model. The same distinction applies to missing: either the field is added to the new screen or the model drops the field. Who chooses which in a conversion is a product decision; the rule only shows that the two sides have diverged and keeps the divergence visible until a decision is made.

That is why an inventory step goes into the conversion plan. Every field the screen to be moved reads from the model is listed, and the constants the old screen writes on its own are marked separately. A screen moved without an inventory gives, in the measurement, the first two results, missing and leftover, together. Once the inventory is done, the check for each moved screen comes down to a single question: are this screen's fields the same as the old screen's fields and the model's fields?

#### The Bounding Measurement

In the middle of the conversion the same model feeds two screens at once: the old screen is still in place, the new screen beside it. The question is whether "is this screen in agreement with the model" can be answered at screen level.

```swift
// main.swift — the same model is checked against two screens
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
    }
}

let task = TaskModel(title: "Tasks", openCount: 5, dueLabel: "tomorrow")
let keys = ["title", "openCount", "dueLabel"]

let legacy = LegacyTaskController()
legacy.bind(task)
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let oldScreen = check("two-screens-old", model: task.values, screen: legacy.screen, keys: keys)
let newScreen = check("two-screens-new", model: task.values, screen: migrated, keys: keys)

countVerdicts(oldScreen + newScreen)
```

```
two-screens-old | LegacyTaskController.title: model=Tasks screen=Tasks -> matching
two-screens-old | LegacyTaskController.openCount: model=5 screen=5 -> matching
two-screens-old | LegacyTaskController.dueLabel: model=tomorrow screen=tomorrow -> matching
two-screens-new | TaskScreenView.title: model=Tasks screen=Tasks -> matching
two-screens-new | TaskScreenView.openCount: model=5 screen=5 -> matching
two-screens-new | TaskScreenView.dueLabel: model=tomorrow screen=- -> missing
matching: 5
stale: 0
missing: 1
leftover: 0
```

The old screen is matching in all three fields, the new screen is matching in two and missing in one. In total, five matching, one missing. From this total one could build a sentence like "eighty-three percent of the application is in agreement", but that sentence does not say which of the screens is consistent. If the user sees only the new screen, the screen is broken in their eyes; if they see only the old one, there is no problem. There is no single answer at screen level, **the rule works per field**. For the same reason, declaring a screen "matching" says that every field written is equal to the model; a field the rule does not know, if it is in the model but stays missing on the screen, shows up only in the count.

This measurement also names the place where the model departs from the framework. **BR2.** The model assumes that every value the old screen writes in `bind` has been written to the screen; the placement of the label text, the font or truncation are not measured. What is measured is which keys the `bind` call writes values to.

In a real conversion the two screens are not shown to the same user at the same time; one leaves the stack, the other enters. In the model both exist, because the rule queries a field, not a screen, and for a rule that gives a result per field, putting the two side by side is enough.

The limit of the measurement is this: the rule does not compare the two screens with each other; it compares each of them separately with the model. There is no direct comparison between the old screen and the new screen; both are measured against the model. If the model itself is wrong, for example if a field is not defined in the model at all, both screens can carry the same gap and the rule finds both of them matching. The scope of the rule is only the known fields of the model.

When the conversion is finished, a last step remains: deleting the old screen. The leftover fields the old screen wrote go away with it. A field the new screen still leaves missing, on the other hand, finds no old screen to compare with from this point on, and the gap can be noticed only through the model. The guarantee incremental conversion gives is that, while the old screen is in place, every field gives the same result on both screens; this guarantee is lost unless the measurement is run after the old screen is deleted.

#### Summary

- Legacy code is converted screen by screen; in the middle of the conversion two frameworks live in the same application and feed the same model.
- The `dueLabel` field not carried to the new screen is `tomorrow` in the model and does not exist on the screen at all: it is measured missing (2 matching, 1 missing).
- When the field is carried over, all three fields are matching (3 matching).
- When the old screen keeps writing the `badge` field dropped from the model, the result is leftover (1 matching, 1 leftover).
- When the same model is checked against two screens, the old screen is matching in three fields and the new screen in two; in total 5 matching, 1 missing. There is no single answer at screen level, the rule works per field.
- This lesson's agreement: **missing**.

#### Next Step

The next lesson is the close of the topic and of the course: Choosing a Framework. We have measured the two frameworks with the same rule; from there we will look at the question of which one to choose and when.

### Choosing a Framework

Source: https://academia.sh/en/courses/uikit/choosing-a-framework

Measuring two screens, one declarative and one imperative, built from the same model and checked with the same rule, showing that agreement is not a criterion for choosing a framework; the closing table of the course's fifteen lessons.

In the previous lesson we moved an old screen to the new framework screen by screen and saw that the rule works per field. The last question of this topic is unavoidably this: if a new screen is to be written, which framework should be chosen? The course's measurement does not answer this question, and the job of this lesson is to show exactly that. The measuring axis has not changed: **agreement**. The core file is the same as in the previous lessons.

```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)")
    }
}
```

#### The Same Model, Two Screens

A profile screen shows three values: a name, an unread count and a status. The model is a single value type. In the declarative framework the screen is a function of this model.

```swift
import SwiftUI

struct ProfileView: View {
    let model: ProfileModel

    var body: some View {
        VStack {
            Text(model.title)
            Text(String(model.unread))
            Text(model.status)
        }
    }
}
```

In the imperative framework the code writes the same three values to three labels one by one.

```swift
import UIKit

final class ProfileController: UIViewController {
    @IBOutlet private var titleLabel: UILabel!
    @IBOutlet private var unreadLabel: UILabel!
    @IBOutlet private var statusLabel: UILabel!

    func render(_ model: ProfileModel) {
        titleLabel.text = model.title
        unreadLabel.text = String(model.unread)
        statusLabel.text = model.status
    }
}
```

The model builds what the code writes to the screen; it is not the framework itself. The values the two screens write stay in the helper file and the names are the same as in the SOURCE snippets.

```swift
// Profile.swift — two screens built from the same model
struct ProfileModel {
    var title: String
    var unread: Int
    var status: String

    var values: [String: String] {
        ["title": title, "unread": String(unread), "status": status]
    }
}

struct ProfileView {
    let model: ProfileModel

    func body(on screen: Screen) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
    }
}

final class ProfileController {
    let screen = Screen("ProfileController")

    func render(_ model: ProfileModel) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
    }
}
```

First event: the same model is given to both screens.

```swift
// main.swift — the same model is written to two screens
let model = ProfileModel(title: "Alice", unread: 3, status: "online")
let keys = ["title", "unread", "status"]

let declarative = Screen("ProfileView")
ProfileView(model: model).body(on: declarative)
let controller = ProfileController()
controller.render(model)

let viaDeclarative = check("same-model-declarative", model: model.values, screen: declarative, keys: keys)
let viaImperative = check("same-model-imperative", model: model.values, screen: controller.screen, keys: keys)

countVerdicts(viaDeclarative + viaImperative)
```

```
same-model-declarative | ProfileView.title: model=Alice screen=Alice -> matching
same-model-declarative | ProfileView.unread: model=3 screen=3 -> matching
same-model-declarative | ProfileView.status: model=online screen=online -> matching
same-model-imperative | ProfileController.title: model=Alice screen=Alice -> matching
same-model-imperative | ProfileController.unread: model=3 screen=3 -> matching
same-model-imperative | ProfileController.status: model=online screen=online -> matching
matching: 6
stale: 0
missing: 0
leftover: 0
```

All six fields are matching. On the imperative side the reason is that the code writes all three fields; on the declarative side it is that the framework recomputes what it reads.

**BR21.** The model assumes that the values the declarative view writes in `body` have been written to the screen, and it builds recomputation in the model by calling `body` again. When the framework does the computation is not measured; this is an assumption.

#### A Field Forgotten on the Imperative Side

While writing the same screen in the imperative framework, it is very easy to drop a line. We set up two events. In the first, the controller never writes the status label. In the second, the screen is set up correctly, then the model changes and the code that sees the change does not rewrite the screen.

```swift
// main.swift — the imperative controller forgets the status, then the model changes
final class ForgetfulProfileController {
    let screen = Screen("ForgetfulProfileController")

    func render(_ model: ProfileModel) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
    }
}

let model = ProfileModel(title: "Alice", unread: 3, status: "online")
let keys = ["title", "unread", "status"]

let forgetful = ForgetfulProfileController()
forgetful.render(model)
let declarative = Screen("ProfileView")
ProfileView(model: model).body(on: declarative)

let missingDeclarative = check("imperative-forgot-declarative", model: model.values, screen: declarative, keys: keys)
let missingImperative = check("imperative-forgot-imperative", model: model.values, screen: forgetful.screen, keys: keys)

let latest = ProfileModel(title: "Alice", unread: 5, status: "online")
let refreshed = Screen("ProfileView")
ProfileView(model: latest).body(on: refreshed)
let careful = ProfileController()
careful.render(model)

let changedDeclarative = check("model-changed-declarative", model: latest.values, screen: refreshed, keys: keys)
let changedImperative = check("model-changed-imperative", model: latest.values, screen: careful.screen, keys: keys)

countVerdicts(missingDeclarative + missingImperative + changedDeclarative + changedImperative)
```

```
imperative-forgot-declarative | ProfileView.title: model=Alice screen=Alice -> matching
imperative-forgot-declarative | ProfileView.unread: model=3 screen=3 -> matching
imperative-forgot-declarative | ProfileView.status: model=online screen=online -> matching
imperative-forgot-imperative | ForgetfulProfileController.title: model=Alice screen=Alice -> matching
imperative-forgot-imperative | ForgetfulProfileController.unread: model=3 screen=3 -> matching
imperative-forgot-imperative | ForgetfulProfileController.status: model=online screen=- -> missing
model-changed-declarative | ProfileView.title: model=Alice screen=Alice -> matching
model-changed-declarative | ProfileView.unread: model=5 screen=5 -> matching
model-changed-declarative | ProfileView.status: model=online screen=online -> matching
model-changed-imperative | ProfileController.title: model=Alice screen=Alice -> matching
model-changed-imperative | ProfileController.unread: model=5 screen=3 -> stale
model-changed-imperative | ProfileController.status: model=online screen=online -> matching
matching: 10
stale: 1
missing: 1
leftover: 0
```

The forgotten `status` field came out **missing** on the imperative screen, the field that was not updated, `unread`, came out **stale**; all six fields of the declarative screen are matching. That is, the imperative framework increases the number of places where the programmer can forget to write. This is not a defect but the price of the framework handing the work back to the programmer: the control over the screen is that much greater too.

#### The Bounding Measurement

This price does not make agreement a criterion for choosing. The rule does not know which framework built the screen; it looks at what the screen shows, and breakage is possible in both directions. A carefully written imperative controller keeps all fields matching. A declarative view, on the other hand, can show a constant text that is not in the model; the constant text is written as is on every recomputation.

```swift
// main.swift — breakage happens in both frameworks
struct BannerProfileView {
    let model: ProfileModel

    func body(on screen: Screen) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
        screen.set("banner", "new")
    }
}

let model = ProfileModel(title: "Alice", unread: 3, status: "online")
let keys = ["title", "unread", "status", "banner"]

let careful = ProfileController()
careful.render(model)
let banner = Screen("BannerProfileView")
BannerProfileView(model: model).body(on: banner)

let carefulResult = check("bound-imperative", model: model.values, screen: careful.screen, keys: keys)
countVerdicts(carefulResult)
let bannerResult = check("bound-declarative", model: model.values, screen: banner, keys: keys)
countVerdicts(bannerResult)
```

```
bound-imperative | ProfileController.title: model=Alice screen=Alice -> matching
bound-imperative | ProfileController.unread: model=3 screen=3 -> matching
bound-imperative | ProfileController.status: model=online screen=online -> matching
bound-imperative | ProfileController.banner: model=- screen=- -> out-of-scope
matching: 3
stale: 0
missing: 0
leftover: 0
bound-declarative | BannerProfileView.title: model=Alice screen=Alice -> matching
bound-declarative | BannerProfileView.unread: model=3 screen=3 -> matching
bound-declarative | BannerProfileView.status: model=online screen=online -> matching
bound-declarative | BannerProfileView.banner: model=- screen=new -> leftover
matching: 3
stale: 0
missing: 0
leftover: 1
```

The imperative screen gave three matching and `banner` stayed out of scope. The declarative screen gave the same three matching and one **leftover**. At first glance one of the two screens looks "more in agreement", but the counts of the two frameworks do not rank them: the same rule, the same model, different mistakes written in code.

**BR22.** The model does not know the framework that built the screen; it only compares the fields the screen shows. The reasons for choosing a framework are not measured, and are not turned into numbers in this lesson.

The real reasons lie elsewhere and all of them are weighed in prose.

- **Team.** A team knowing the declarative framework means fewer lines written per screen; if the team knows the imperative framework, the learning cost can exceed the gain the new framework brings.
- **Legacy code.** Rewriting a working screen carries risk. The incremental path in the previous lesson shrinks this risk per screen.
- **Platform version.** The declarative framework is not available on every target version; the lowest version to be supported is decided before choosing the framework.
- **Custom drawing.** A piece drawn by hand or animated with layers may need the control the imperative framework offers; the declarative screen takes it in by wrapping.

None of these four reasons is a number, and our measurement is not enough to make this decision.

#### Summary

- When the same model is built with both frameworks, both screens are matching in six fields (6 matching).
- On the imperative side a field not written is measured missing and a field that does not follow the model is measured stale; the declarative side is matching in the same events (10 matching, 1 stale, 1 missing).
- Breakage happens in both frameworks: the declarative screen's constant text that is not in the model gives leftover (3 matching, 1 leftover).
- Agreement is not a criterion for choosing a framework; the choice is weighed in prose through team, legacy code, platform version and custom drawing.
- This lesson's agreement: **matching**.

#### Course Wrap-Up

The course measured fifteen lessons with a single question: does this screen agree with the model? Each lesson's event, agreement and bounding measurement are below. The agreement cells are taken from the source lesson's summary.

| Lesson | Event | Agreement | Bounding measurement |
|---|---|---|---|
| The View Controller Concept | loading, read mark, folder change, draft arrival, cell reuse | all four (3/2/2/2) | formatted text is not counted equal; a field absent on both sides is not counted |
| Lifecycle Methods | value written only at load, model changes underneath | stale | the model builds the call order, not the framework |
| Navigation Controllers | push and pop, highlight of a removed selection | leftover | the popped screen is out of scope |
| Presentation Styles | modal presented note, field the presenting screen does not write | missing | only the visible screen is looked at |
| The Visual Editor and Code-Built Interfaces | broken outlet, placeholder text | stale | placeholder stays on a label the code does not write |
| Outlets and Actions | action that writes the model but not the label | stale | formatted text comes out stale against the raw value |
| Constraint-Based Layout | constraint constant not updated | stale | priority and ambiguity are not in the model |
| Stack Views | subview not hidden | leftover | hiding and removing write the same value |
| Table Views | accessory left on a reused cell | leftover | queue size is not measured |
| Collection Views | item inserted without notifying | missing | position calculation is not measured |
| The Delegate Pattern | unconnected delegate | stale | the person who forgot is not read |
| Drawing and Layer Animation | unwanted redraw request | stale | animation duration is not measured |
| Declarative and Imperative Interfaces Together | root view not reassigned | stale | declarative recomputation is an assumption |
| Incremental Migration of Legacy Code | field not carried over, removed badge | missing | no single answer at screen level |
| Choosing a Framework | same model, forgotten field | matching | agreement is not a criterion for choosing |

The distribution of the fourteen lessons other than the opening lesson: 1 matching, 7 stale, 3 missing, 3 leftover. The core's 3/2/2/2 distribution is a separate count: the core counts the nine fields in the opening lesson, while the distribution counts the agreement of fourteen lessons; the two are not the same thing.

The next course (M30/K05) covers data persistence, network requests and concurrency. The measuring axis of this course does not continue there; K05 asks its own question.

