Building an app with a document browser
At a glance
| Item |
Summary |
| Purpose |
Creates, imports, opens, edits, and closes UTF-8 text documents through UIDocumentBrowserViewController. |
| App architecture |
A browser scene routes incoming file intents to a browser/controller boundary; a UIDocument subclass owns file state, and an editor controller owns visible editing. |
| Main patterns |
Document model, scene intent routing, weak delegate, observer-backed state, and framework-provided document transitions. |
| Project style |
Storyboard-based UIKit app with five Swift files and one focused, app-defined delegate protocol. |
Project structure
Document Browser/
├── BrowserSceneDelegate.swift
├── AppDelegate.swift
├── DocumentBrowserViewController.swift
├── TextDocument.swift
├── TextDocumentViewController.swift
└── Base.lproj/
├── Main.storyboard
└── LaunchScreen.storyboard
Structure observations
- Browser routing, document persistence, and editing UI are separate source-file roles.
- The storyboard supplies the browser root and navigation/editor hierarchy; the browser injects the opened
TextDocument.
AppDelegate contains application lifecycle plumbing, not the feature’s composition logic.
Overall architecture
flowchart LR
FileIntent["Scene URL context"] --> Scene["BrowserSceneDelegate"]
Scene --> Browser["DocumentBrowserViewController"]
Browser -->|"create / import / pick"| Document["TextDocument : UIDocument"]
Browser -->|"instantiate and inject"| Editor["TextDocumentViewController"]
Browser --> Transition["UIDocumentBrowserTransitionController"]
Editor -->|"edit + updateChangeCount"| Document
Document -->|"weak events"| Editor
Document --> Storage["UTF-8 file contents"]
Reference code
Document Browser/DocumentBrowserViewController.swift:128 — the browser composes the open-document session, connects progress to the transition, injects the model, and presents only after a successful open.
transitionController = transitionController(forDocumentAt: documentURL)
let doc = TextDocument(fileURL: documentURL)
transitionController!.targetView = documentViewController.textView
transitionController!.loadingProgress = doc.loadProgress
documentViewController.document = doc
documentViewController.document.open { success in
if success {
self.transitionController!.loadingProgress = nil
self.present(docNavController, animated: true)
}
}
Interpretation
The browser is the composition and presentation boundary, while TextDocument is the persistence/state boundary. The editor does not read files itself; it mutates the document and tells UIDocument that a change occurred.
Ownership and state
classDiagram
BrowserSceneDelegate o-- DocumentBrowserViewController : finds root
DocumentBrowserViewController *-- UIDocumentBrowserTransitionController : current transition
DocumentBrowserViewController --> TextDocument : creates
DocumentBrowserViewController --> TextDocumentViewController : injects document
TextDocumentViewController o-- TextDocument : strong document reference
TextDocument --> TextDocumentViewController : weak delegate
TextDocument *-- Progress : loadProgress
TextDocument *-- NotificationToken : document state observer
TextDocumentViewController *-- NotificationToken : keyboard observers
Ownership evidence
Document Browser/TextDocument.swift:28 — the document owns text/progress and observer state, but deliberately keeps its callback recipient weak.
public var text = "" {
didSet {
delegate?.textDocumentUpdateContent(self)
}
}
public weak var delegate: TextDocumentDelegate?
public var loadProgress = Progress(totalUnitCount: 10)
private var docStateObserver: Any?
private var transfering: Bool = false
| Owner |
Object or state |
Relationship |
Mutation authority |
| Browser scene delegate |
Main browser scene marker and incoming URL context |
Routes, does not own document content |
Scene callbacks and private URL router |
| Document browser controller |
Current transition controller and document presentation flow |
Creates/retains transition; creates document/editor session |
Browser delegate methods and presentDocument |
TextDocumentViewController |
Current document reference, text UI, progress UI, keyboard tokens |
Retains document for visible session |
Text callbacks and editor actions |
TextDocument |
Text, load progress, transfer flag, state observer |
Owns persistent model state |
UIDocument lifecycle and private state handlers |
TextDocument.delegate |
Editor callback target |
Weak, non-owning |
Assigned when editor receives document |
Class and protocol design
| Type or protocol |
Responsibility |
Depends on or conforms to |
BrowserSceneDelegate |
Mark/find the browser scene and route open-in-place or import URLs |
UIWindowSceneDelegate, browser root |
DocumentBrowserViewController |
Create/import/pick documents, construct editor presentation, vend transition animation |
UIDocumentBrowserViewControllerDelegate, UIViewControllerTransitioningDelegate |
TextDocument |
Encode/decode UTF-8 and translate document-state changes into semantic events |
UIDocument, NotificationCenter |
TextDocumentDelegate |
Describe editing, content, transfer, and save-failure events |
Class-only (AnyObject) callback contract |
TextDocumentViewController |
Bind text/progress UI to one document and mark edits |
UITextViewDelegate, TextDocumentDelegate |
TextDocumentDelegate is genuine protocol-oriented design: the model reports semantic document events without importing editor details. The sample has one concrete conformer, so the value is separation and weak ownership rather than multiple implementations.
Access control
| Symbol |
Access |
Verified effect |
Why it fits this design |
BrowserSceneDelegate.openURLContext |
private |
Only the scene delegate and its same-file lexical scope can invoke the routing helper. |
Keeps URL interpretation behind scene callbacks. |
TextDocument.text, delegate, loadProgress |
Declared public |
Named as externally visible members, but effective reach is limited because TextDocument itself is internal. |
Makes the document’s collaboration surface explicit inside this app sample. |
TextDocument.delegate |
public weak |
Does not retain the editor. |
Avoids a document ↔︎ editor retain cycle. |
| Observer token, transfer flag, state/conflict helpers |
private |
Only TextDocument can mutate lifecycle bookkeeping. |
Protects document-state invariants. |
Keyboard observer tokens and setupNotifications |
private |
Notification setup remains an editor implementation detail. |
Keeps UI-lifecycle mechanics out of callers. |
Controllers, protocol, outlets, and document property |
implicit internal |
Accessible within the app target. |
No package/framework API is being designed. |
Reference code
Document Browser/BrowserSceneDelegate.swift:47 — the public scene callbacks funnel both connection-time and later URL events into one private router.
private func openURLContext(_ urlContext: UIOpenURLContext) {
guard let documentBrowserViewController =
window?.rootViewController as? DocumentBrowserViewController else {
fatalError("*** The root view is not a document browser ***")
}
documentBrowserViewController.revealDocument(
at: urlContext.url,
importIfNeeded: !urlContext.options.openInPlace
) { revealedURL, error in
// Resolve the URL, then call presentDocument(at:).
}
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Scene identification and external URL intent |
BrowserSceneDelegate |
The scene callback owns connection context and open-in-place metadata. |
| New/imported/picked document presentation |
DocumentBrowserViewController |
The browser owns provider URLs, storyboard presentation, and transition APIs. |
| UTF-8 serialization and conflict/state handling |
TextDocument |
These are document invariants independent of the current view. |
| Text edits, change counting, progress/error UI |
TextDocumentViewController |
These behaviors require editor controls and presentation context. |
| Keyboard inset animation |
TextDocumentViewController |
Layout response belongs with the text view it adjusts. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Document model |
Document Browser/TextDocument.swift:26 |
Centralizes coordinated persistence and document state. |
| Scene intent router |
Document Browser/BrowserSceneDelegate.swift:30 |
Handles startup and later URL contexts through one route. |
| Weak delegate |
Document Browser/TextDocument.swift:16 |
Sends semantic state events without owning the editor. |
| Observer-backed state |
Document Browser/TextDocument.swift:45 |
Converts framework notifications into editing/transfer/conflict behavior. |
| Composition root in browser |
Document Browser/DocumentBrowserViewController.swift:109 |
Builds one document/editor/transition session at the navigation boundary. |
| Framework transition adapter |
Document Browser/DocumentBrowserViewController.swift:156 |
Reuses the browser transition controller for presentation and dismissal. |
Naming conventions
- Types name framework roles directly:
DocumentBrowserViewController, TextDocument, and TextDocumentViewController.
- Callback methods use the subject-event form:
textDocumentTransferBegan and textDocumentSaveFailed.
- Commands use explicit verbs and objects:
presentDocument, openURLContext, resolveDocumentConflict, and setupNotifications.
- State names are concrete (
loadProgress, docStateObserver); transfering is source spelling and represents a Boolean phase flag.
Architecture takeaways
- Put file-format and document-state invariants in the
UIDocument subclass, not the editor.
- Let the document browser construct each open-session graph because it owns URLs, transitions, and presentation.
- Use a weak, semantic delegate to keep the document independent of concrete UI.
- Keep lifecycle flags and observer tokens private; expose only the model/progress surface collaborators need.
Source map
| Source file |
Relevant symbols |
Document Browser/BrowserSceneDelegate.swift |
Browser scene marker and URL routing |
Document Browser/DocumentBrowserViewController.swift |
Creation/import/picking, composition, transition |
Document Browser/TextDocument.swift |
UTF-8 document model, state observer, delegate protocol |
Document Browser/TextDocumentViewController.swift |
Editing, change counting, progress/error and keyboard UI |
Document Browser/AppDelegate.swift |
Application lifecycle |