---
title: 'Outlets and Actions'
source: 'https://academia.sh/en/courses/uikit/outlets-and-actions'
course: 'UIKit and Bridging'
language: en
updated: '2026-10-21T05:03:16+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates the direction in which an outlet works from the direction in which an action works'
    - 'Measures an action that updates the model but does not write the label as stale'
    - 'Shows that an action that writes both the model and the label is matching'
    - 'Explains why comparing formatted text with the raw value comes out stale'
---

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