Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-26View on Apple Developer

Disabling the pull-down gesture for a sheet

At a glance

Item Summary
Purpose Prevents interactive sheet dismissal while text has unsaved edits and offers save/discard/cancel when dismissal is attempted.
App architecture The root owns committed text; the edit controller owns a transactional copy and derived dirty state; two delegate seams coordinate commit/cancel and adaptive-presentation attempts.
Main patterns Edit transaction, derived dirty state, weak delegate, adaptive-presentation guard, and confirmation flow.
Project style Three-file storyboard UIKit app with one focused app-defined protocol.

Project structure

SheetPresentations/
├── RootViewController.swift
├── EditViewController.swift
│   └── EditViewControllerDelegate
├── AppDelegate.swift
└── Base.lproj/Main.storyboard

Structure observations

  • Committed display state and draft editing state have different owners.
  • RootViewController wires the sheet collaboration during segue preparation.
  • EditViewController contains both text-editing and dismissal-protection policy because both depend on hasChanges.

Overall architecture

Reference code

SheetPresentations/RootViewController.swift:32 — the composition point connects both delegate directions and copies committed text into the edit transaction.

override func prepare(
    for segue: UIStoryboardSegue,
    sender: Any?
) {
    guard segue.identifier == "Edit" else { return }

    let navigationController =
        segue.destination as! UINavigationController
    let editor =
        navigationController.topViewController
            as! EditViewController

    navigationController.presentationController?
        .delegate = editor
    editor.delegate = self
    editor.originalText = text
}

Interpretation

The root is the commit owner; the editor is a draft/session owner. Setting the editor as presentation-controller delegate lets the same owner that knows whether changes exist decide what an attempted gesture should do.

Ownership and state

Ownership evidence

SheetPresentations/EditViewController.swift:20 — the editor owns the original/draft pair, derives dirty state, and keeps the commit recipient weak.

var originalText = "" {
    didSet {
        editedText = originalText
    }
}

var editedText = "" {
    didSet {
        viewIfLoaded?.setNeedsLayout()
    }
}

var hasChanges: Bool {
    originalText != editedText
}

weak var delegate: EditViewControllerDelegate?
Owner Object or state Relationship Mutation authority
RootViewController Committed text Source of truth outside editing session Finish delegate callback
EditViewController originalText and editedText Owns draft transaction Injection and text-view callbacks
EditViewController hasChanges Derived, not separately stored Computed from transaction pair
Presented navigation controller Edit controller lifetime Framework container ownership Presentation/dismissal
Editor delegate reference Root callback target Weak, non-owning Root assigns during segue
Presentation controller delegate Editor callback target Framework collaboration Root wires during segue

Class and protocol design

Type or protocol Responsibility Depends on or conforms to
EditViewControllerDelegate Report cancel or successful finish to commit owner Class-only AnyObject
RootViewController Own committed text, compose sheet, commit/dismiss EditViewControllerDelegate
EditViewController Own draft, edit text, protect dismissal, present confirmation UITextViewDelegate, UIAdaptivePresentationControllerDelegate
AppDelegate Hold legacy window UIApplicationDelegate

The app-defined protocol is a useful semantic boundary: the editor reports outcome without reaching into root storage or dismissing via a global presenter. Class-only conformance enables the weak reference.

Access control

Symbol Access Verified effect Why it fits this design
Editor delegate implicit internal weak Does not retain the root controller. Prevents a presented editor → presenting owner cycle.
EditViewControllerDelegate implicit internal, class-only Available inside the app target only. No public framework contract is intended.
Committed/original/edited text implicit internal var Any module code could mutate transaction state. Supports simple segue wiring; production code could use private setters/configuration.
hasChanges implicit internal read-only computed property Callers can inspect but not assign it. Derived state has no independent mutation path.
Outlets weak, implicit internal View hierarchy owns controls. Correct storyboard ownership.
confirmCancel implicit internal Other module code could trigger confirmation. Only editor events use it; private would match actual ownership.
private / fileprivate / public / private(set) Not used No finer access boundary. Sample centers on interaction contract rather than library encapsulation.

Reference code

SheetPresentations/EditViewController.swift:53 — one derived flag drives both save availability and system interactive-dismissal policy.

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    textView.text = editedText
    saveButton.isEnabled = hasChanges
    isModalInPresentation = hasChanges
}

func textViewDidChange(_ textView: UITextView) {
    editedText = textView.text
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Committed text and final update RootViewController Root screen is the durable UI source of truth.
Sheet graph/delegate wiring RootViewController.prepare(for:) Presenting owner has navigation/presentation references.
Draft copying and dirty-state derivation EditViewController Specific to one editing session.
Pull-down prevention and attempted-dismiss handling EditViewController Requires dirty state and confirmation presentation context.
Save/cancel outcome App-defined delegate Separates edit workflow from root implementation.
Modal containment/dismiss gesture UIKit presentation controller Framework owns sheet interaction mechanics.

Design patterns

Pattern Source evidence Purpose or tradeoff
Edit transaction SheetPresentations/EditViewController.swift:20 Keeps draft changes separate until explicit commit.
Derived dirty state SheetPresentations/EditViewController.swift:32 Avoids synchronizing a second Boolean state variable.
Weak delegate SheetPresentations/EditViewController.swift:38 Reports semantic outcome without owning the presenter.
Composition at segue SheetPresentations/RootViewController.swift:32 Wires dependencies at the presentation boundary.
Adaptive-presentation guard SheetPresentations/EditViewController.swift:59 Disables pull-down only when losing data is possible.
Confirmation flow SheetPresentations/EditViewController.swift:97 Offers save/discard/cancel based on how dismissal began.

Naming conventions

  • RootViewController and EditViewController distinguish committed display from draft workflow.
  • Delegate callbacks use subject/outcome names: editViewControllerDidCancel and editViewControllerDidFinish.
  • originalText, editedText, and hasChanges make transaction phases explicit.
  • confirmCancel(showingSave:) describes UI intent; a richer enum could replace the Boolean if more entry paths existed.

Architecture takeaways

  • Keep committed data and editable draft in separate owners.
  • Derive dirty state from original and edited values instead of storing a second flag.
  • Let the dirty-state owner control isModalInPresentation and attempted-dismiss confirmation.
  • Use a weak, class-bound delegate to return commit/cancel outcomes.
  • Narrow draft mutation and confirmation helpers in production while keeping the semantic delegate visible.

Source map

Source file Relevant symbols
SheetPresentations/RootViewController.swift Committed state, sheet composition, commit/cancel
SheetPresentations/EditViewController.swift Draft state, delegate protocol, dismissal guard, confirmation
SheetPresentations/AppDelegate.swift Legacy window lifecycle
SheetPresentations/Main.storyboard Root/edit navigation and segue