Skip to content
academia.sh

Lesson 05 / 15

The Visual Editor and Code-Built Interfaces

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.

Published Updated

In this lesson

  • Separates what building a screen in the visual editor or in code changes for the controller
  • Measures that a label with a broken outlet silently goes unwritten through an optional chain
  • Shows that a code-built label is not affected by this path
  • Recognizes the placeholder text the editor saves in the file, which has no counterpart in the model, as leftover
Contents

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.

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:

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.

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

// 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:

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.

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close