Skip to content
academia.sh

Lesson 04 / 15

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.

Published Updated

In this lesson

  • Separates push presentation from modal presentation by where the screen is placed
  • Measures a value entered in a modal presentation being written to the model but not to the screen beneath
  • Shows that the fields the screen beneath already had stay matching during the presentation
  • Separates the measurement looking only at the visible screen and not counting the dismissed one
Contents

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.

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.

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.

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

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

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.

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close