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
flowchart LR
Root["RootViewController"] -->|"segue: inject originalText"| Edit["EditViewController"]
Root -->|"EditViewControllerDelegate"| Edit
Presentation["UIPresentationController"] -->|"attempted dismiss"| Edit
Edit --> Draft["originalText + editedText"]
Draft --> Dirty["hasChanges"]
Dirty -->|"true"| Guard["isModalInPresentation"]
Edit -->|"save"| Root
Edit -->|"cancel / discard"| Root
Root -->|"commit editedText"| Saved["Root text"]
Root -->|"dismiss"| Presentation
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
classDiagram
RootViewController *-- String : committed text
UINavigationController *-- EditViewController : presented child
EditViewController *-- String : originalText
EditViewController *-- String : editedText
EditViewController --> RootViewController : weak delegate
UIPresentationController --> EditViewController : adaptive delegate
EditViewController o-- UITextView : weak IBOutlet
EditViewController o-- UIBarButtonItem : weak outlets
EditViewController --> UIAlertController : temporary confirmation
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 |