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

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

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

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