SwiftUI Document API in iOS 27: Async Reading, Incremental Saves, and Creation Sources
- Author
- Ehsan Azish · 3NSOFTS
- Updated
- September 2026
- Read time
- 16 min read
- Level
- Intermediate
- Platform
- iOS 27+, SwiftUI, Observation, Uniform Type Identifiers
Implementation Notes
- ~/ What broke: A production edge case that generic tutorials skip.
- ~/ What to do: Ship the production fix with clear state, errors, and fallback behavior.
SwiftUI’s expanded Document API in iOS 27 is more than a new spelling for FileDocument. It separates the in-memory document from background readers and writers, uses explicit snapshots, supports progress and incremental work, exposes document creation context, and lets an app work directly with coordinated file URLs when another framework needs them.
The result is a better fit for large documents, packages, media files, and editors whose UI must remain responsive while disk work continues.
Document,ReadableDocument,WritableDocument, and related iOS 27 APIs are beta as of September 2026. Compile examples with the matching Xcode SDK and recheck signatures before release.
Choose the right document model
There are now three related protocols:
ReadableDocumentfor viewers that open but do not modify a file;WritableDocumentfor creating, saving, and exporting;Document, a convenience protocol combining both.
Unlike the value-oriented FileDocument, the new document is a reference type. That gives SwiftUI stable identity across updates and works naturally with Observation:
import Observation
import SwiftUI
import UniformTypeIdentifiers
@Observable
final class NoteDocument: Document {
static let readableContentTypes: [UTType] = [.markdown]
static let writableContentTypes: [UTType] = [.markdown]
var text = ""
}
Use the new model when the app benefits from asynchronous or incremental I/O, direct URL access, progress, multiple creation paths, or fine-grained observed properties. Existing small FileDocument apps do not need an automatic rewrite merely because a new API exists.
Understand the snapshot boundary
The central architecture is a snapshot boundary between UI state and disk work.
During a save:
- SwiftUI calls
snapshot(contentType:)on the main actor. - The document returns an immutable, sendable representation.
- SwiftUI obtains the writer.
- The writer performs coordinated I/O in the background.
During a read:
- A reader performs disk work in the background.
- It returns a snapshot.
- SwiftUI passes that snapshot to
apply(snapshot:previous:). - The document updates observable UI state on the main actor.
This separation prevents a writer from reading live mutable UI state while the person continues editing.
Choose a snapshot that is:
- complete enough to save consistently;
- immutable after capture;
Sendableacross isolation boundaries;- cheaper to compare than the entire view model;
- independent of views and environment objects.
For a text document, String may be enough. For a package editor, use a dedicated value containing metadata plus references or data for changed members.
A minimal writable document
Apple’s FileWrapperDocumentWriter handles the common case where a snapshot becomes a FileWrapper:
@Observable
final class NoteDocument: WritableDocument {
static let writableContentTypes: [UTType] = [.markdown]
var text = ""
func writer(
configuration: sending WriteConfiguration
) -> sending FileWrapperDocumentWriter<String> {
FileWrapperDocumentWriter(configuration) { snapshot, _ in
FileWrapper(
regularFileWithContents: Data(snapshot.utf8)
)
}
}
@MainActor
func snapshot(contentType: UTType) async throws -> sending String {
text
}
}
The snapshot method should capture state, not perform a long export. Expensive serialization belongs in the background writer.
If the app writes multiple types, inspect contentType and define every supported format in writableContentTypes. Do not declare a format simply to make it appear in a save panel; the writer must produce a valid representation for it.
Add reading without blocking the editor
For a simple file wrapper, the reader decodes data into a snapshot and the document applies it:
@Observable
final class MarkdownDocument: ReadableDocument {
static let readableContentTypes: [UTType] = [.markdown]
var text = ""
func reader(
configuration: sending ReadConfiguration
) -> sending FileWrapperDocumentReader<String> {
FileWrapperDocumentReader(configuration) { fileWrapper in
guard let data = fileWrapper.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
return String(decoding: data, as: UTF8.self)
}
}
@MainActor
func apply(
snapshot: sending String,
previous: sending String?
) async throws {
text = snapshot
}
}
previous enables smarter updates. A complex editor can compare the newly read snapshot with the previous one and update only changed model areas rather than rebuilding every object.
Treat decoding errors as document errors. Do not silently replace corrupt or unsupported content with an empty document, because the next autosave could overwrite the original.
Incremental writing for packages
A package is a directory presented as one document. Packages work well for projects containing a manifest, media, previews, and independently changing resources.
The new writer receives current and previous snapshots. Use that comparison to avoid rewriting unchanged members:
struct ProjectSnapshot: Sendable, Equatable {
let manifest: Data
let pages: [PageID: Data]
let changedPageIDs: Set<PageID>
}
A production writer can then:
- validate the destination content type;
- reuse unchanged package members from the previous state;
- write changed members into temporary locations;
- report progress as meaningful units finish;
- let coordinated file access replace the destination safely.
Do not mutate the live ProjectDocument from the writer. If a save discovers something the UI must know, return a result through an application-controlled channel after the coordinated write completes.
Progress is part of the model
The Document APIs can report long read and write progress using Foundation’s Subprogress support. This matters for PDF processing, media packages, large datasets, and export formats generated by another framework.
Good progress:
- is based on measurable work units;
- advances monotonically;
- distinguishes preparing, writing, and finalizing when useful;
- remains cancellable where the underlying operation supports cancellation;
- never claims completion before coordinated replacement succeeds.
Avoid a timer-based fake percentage. Indeterminate progress is more honest when total work cannot be estimated.
Use creation sources for purposeful new-document flows
DocumentCreationSource lets an iOS document browser offer more than one way to begin. Apple’s pattern declares stable sources and pairs them with NewDocumentButton:
extension DocumentCreationSource {
static let blank = Self(id: "blank-document")
static let template = Self(id: "template-document")
}
DocumentGroupLaunchScene("Documents") {
NewDocumentButton("Blank Document", source: .blank)
NewDocumentButton("New from Template", source: .template)
}
The document receives the creation source through its configuration or creation context. Use it to establish initial state or present the next relevant workflow.
Keep identifiers stable once released. They are routing values, not localized display strings. The visible button labels can change without changing the source identifier.
Do not put network-dependent template download into the document initializer. Create a valid local starting state first, then run import or download as a visible, recoverable operation.
Autosave depends on undo
A subtle but important rule in Apple’s documentation: without registered undo actions, SwiftUI does not know that user-facing edits require autosave.
Read the active UndoManager from the environment and route mutations through document methods that register the inverse operation:
@Observable
final class ProjectDocument: Document {
// Reader, writer, and content-type declarations omitted here.
var title = "Untitled"
@MainActor
func rename(to newTitle: String, undoManager: UndoManager?) {
let previousTitle = title
undoManager?.registerUndo(withTarget: self) { document in
document.rename(to: previousTitle, undoManager: undoManager)
}
title = newTitle
}
}
struct ProjectEditor: View {
@Environment(\.undoManager) private var undoManager
let document: ProjectDocument
var body: some View {
Button("Use Working Title") {
document.rename(to: "Working Title", undoManager: undoManager)
}
}
}
In real code, centralize this pattern so keyboard edits, inspector changes, drag operations, and accessibility actions all participate in the same undo and autosave model.
Test undo and redo after reopening a document, after switching windows, and while a save is in flight. A UI that visually changes but never registers an edit is a data-loss risk.
Direct URL access and file coordination
URLDocumentConfiguration exposes the open document’s URL, modification metadata, and a way to create a file coordinator. This is useful when a framework such as AVFoundation, Core Graphics, or PDFKit needs a URL rather than an in-memory wrapper.
Direct access does not mean bypassing coordination. Obtain the coordinator from the configuration and keep access within the documented read or write lifecycle. Do not cache a security-scoped or coordinated URL in a global singleton and assume it remains valid forever.
If a framework supports incremental output, combine it with progress and temporary-file replacement. Never let partially written output become the canonical document.
DocumentGroup integration
Use DocumentGroup or DocumentGroupLaunchScene as the app’s first scene to receive the document infrastructure: browser UI, coordinated opening and saving, commands, multiwindow behavior, edited state, and conflict handling.
On iOS, Apple instructs document-browser apps to enable UISupportsDocumentBrowser in the information property list. Register every supported UTType and set the bundle document role correctly:
Editorfor documents the app modifies;Viewerfor read-only documents.
Test files arriving from Files, AirDrop, Share Sheet, iCloud Drive, and another provider. A document app is not production-ready if it works only with files created inside its own sandbox.
Migration from FileDocument
Do not combine the API migration with a file-format redesign. Preserve the format first.
- Extract current encode and decode logic from
fileWrapper(configuration:)andinit(configuration:). - Add golden-file tests for existing documents.
- Define sendable read and write snapshots.
- Move disk work into a reader and writer.
- Adopt an observable reference-type document.
- Wire every mutation through undo.
- Compare output byte-for-byte or semantically, depending on the format.
- Only then add incremental saving or new creation sources.
Keep the old path when supporting earlier operating systems. A shared codec can serve both FileDocument and the new readers/writers without duplicating format rules.
Production failure modes
The snapshot contains reference types. The writer can observe mutations after capture. Prefer immutable sendable values.
Autosave never runs. The UI changed state without registering undo.
Every small edit rewrites gigabytes. Use package structure and compare current and previous snapshots.
A corrupt file becomes a blank document. Surface a recoverable open error and preserve the original.
Progress reaches 100% before replacement. Include finalization in the work model.
A creation source performs hidden network work. Start locally and make external work explicit.
The app keeps the file URL forever. Respect the lifetime and coordination model supplied by the document configuration.
Frequently asked questions
Does Document replace FileDocument?
It provides a more capable architecture for the iOS 27 generation, especially for reference models, background I/O, direct URLs, progress, and incremental work. Existing FileDocument apps can remain appropriate for small files and earlier deployment targets.
Must read and write snapshots use the same type? No. Apple documents them as independent. Use the smallest representation appropriate to each direction.
Why is the document a class? The new protocols require reference types so SwiftUI can preserve identity, while Observation can update only views that depend on changed properties.
Can a read-only app use the document browser?
Yes. Conform to ReadableDocument, use the viewer-oriented DocumentGroup initializer, and set the bundle document role to Viewer.
Can I export PNG or PDF from the same document? Yes, when the app declares the writable content types and its writer produces valid output for each. Export formats may use a different serialization path from the editable native format.
Primary references
- Creating a document-based app
- ReadableDocument
- WritableDocument
- DocumentCreationSource
- What’s new in SwiftUI — WWDC26
The new Document API is valuable because it creates a clean concurrency boundary. The UI owns an observable document, snapshots cross into background readers and writers, coordinated I/O protects the file, and undo tells SwiftUI when an edit deserves autosave. Preserve that separation and the API scales from a note to a complex package editor.