SwiftUI's Document API got a fundamental update with iOS 27: Apple introduced two new protocols for document-based apps, ReadableDocument and WritableDocument. In this post I walk through the difference between these protocols and FileDocument/ReferenceFileDocument, the incremental/asynchronous write mechanism, and the multi-document creation flow with DocumentCreationSource, step by step, based on Apple's WWDC26 resources and an independent write-up that corroborates them.
💡 Pro Tip: If you're starting a project whose new deployment target is 27+, start directly withReadableDocument/WritableDocumentinstead ofFileDocument— migrating back later is far more expensive than starting there.
Table of Contents
- Why the new protocols arrived
- Migrating FileDocument → ReadableDocument (a side-by-side example)
- What the old FileDocument side used to look like
- The role of PageSnapshot and DocumentReader
- ReferenceFileDocument → WritableDocument
- A concrete Writer type
- Exporting the same document as PNG too
- Incremental and asynchronous writing
- Computing the diff with the previous parameter
- Progress reporting with Subprogress
- What the consuming keyword means
- DocumentCreationSource and NewDocumentButton
- Adding multiple sources to the scene
- iCloud/Files integration pitfalls
- Why autosave doesn't trigger without a registered undo action
- iOS 26 backward compatibility
- What to do with a mixed deployment target
- FAQ
- What replaces FileDocument now?
- What's the difference between ReadableDocument and WritableDocument?
- How do I incrementally save a large file in SwiftUI?
- How do you define multiple new-document sources in a DocumentGroup?
- What happens if I don't register an undo action?
- Do I need to change my old FileDocument code right away?
- Update (September 2026)
- Conclusion
- Sources
Why the new protocols arrived
Apple's official WWDC26 guide describes the expanded SwiftUI Document API this way: "The expanded SwiftUI Document API gives you direct control over the structure of your saved documents. For reading and writing, conform to WritableDocument and ReadableDocument…" This sentence sums up the origin point of the new protocols: the old FileDocument/ReferenceFileDocument pair didn't give you fine-grained control over the document's structure on disk.
Apple's WWDC26 session 269 walkthrough directly backs up the same motivation: "First, the write method is nonisolated and asynchronous. This lets me perform expensive disk writing operations in the background, so the app stays responsive. I write only the parts of the package that actually need updating, by comparing the current and the previous snapshots." So the reason isn't one thing: disk-structure control, incremental writes, and non-blocking background I/O.
On top of that, the new Document type is now a reference type marked @Observable. In onmyway133's words: "The document is a reference type marked @Observable, so SwiftUI does not recreate it on every change."
The WWDC26 guide page doesn't quote FileDocument directly, but Apple's session 269 summary explicitly mentions this protocol: "Expanded document APIs for SwiftUI apps, including the new DocumentCreationSource API for custom new-document flows, performance improvements for reading and writing large documents, and first-class support for direct document URL access via the FileDocument and ReferenceFileDocument protocols." onmyway133's write-up summarizes this more sharply: "two new protocols replace FileDocument and ReferenceFileDocument for new code." So the new protocols are a pair that replaces the old ones for newly written code — a distinction that matters later (specifically, in the iOS 26 compatibility section).
Migrating FileDocument → ReadableDocument (a side-by-side example)
The StickerDocument definition below, based on the WWDC26 session's example, shows what conforming to WritableDocument looks like.
ReadableDocument is defined in the WWDC26 session like this: "ReadableDocument is a twin to WritableDocument. Here's how they compare. Each protocol requires a list of supported content types. WritableDocument provides a snapshot and ReadableDocument knows how to apply it." The same session clarifies disk access this way: "ReadableDocument's friend is DocumentReader, which does all the disk-related heavy lifting." So it works on a twin-protocol model, and applying snapshots read from disk back onto the document is handled through a DocumentReader that owns that job.
1@Observable2final class StickerDocument: WritableDocument {3 static let writableContentTypes: [UTType] = [.stickerDocument]4 5 @MainActor6 func snapshot(contentType: UTType) async throws -> sending PageSnapshot {7 // encode current state into a PageSnapshot8 }9 10 func writer(configuration: sending WriteConfiguration) -> sending Writer {11 // return a Writer that knows how to persist a PageSnapshot12 }13}The @MainActor marking on the definition above shows that taking a snapshot runs on the main thread; the nonisolated marking you'll see on the write side further down shows the opposite — that operation can run without being tied to any actor.
What the old FileDocument side used to look like
To complete this "side-by-side example" comparison, the same StickerDocument type's conformance to the old FileDocument protocol used to look like this — a struct that declares readableContentTypes, does reading in a single pass inside init(configuration:), and writing in a single pass inside fileWrapper(configuration:):
1struct StickerDocument: FileDocument {2 static var readableContentTypes: [UTType] { [.stickerDocument] }3 4 init(configuration: ReadConfiguration) throws {5 // decode the data inside configuration.file (a FileWrapper) into the struct in one pass6 }7 8 func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {9 // encode the struct's current state into a new FileWrapper in one pass10 }11}Side by side with the WritableDocument example above, the difference is clear: the old side runs init(configuration:) and fileWrapper(configuration:) synchronously in one piece, while the new side prepares snapshot(contentType:) on @MainActor and pushes the actual disk write through a separate, nonisolated writer (shown further down).
The role of PageSnapshot and DocumentReader
In the WWDC26 session 269 example, the type returned by snapshot(contentType:) above is concretely defined as: struct PageSnapshot { var background: Image; var metadata: StickerPlacements; var stickers: [Image] }. So the snapshot freezes the document's state — background image, metadata with sticker positions, and the sticker images — into one value type; ReadableDocument's apply(snapshot:previous:) applies this PageSnapshot, read back from disk, onto the document object.
In the same session, StickerDocument's conformance to ReadableDocument is given as a separate extension: extension StickerDocument: ReadableDocument {}. This shows that WritableDocument and ReadableDocument can be defined as two separate conformance blocks on the same class — meaning reading and writing responsibilities can be separated in the code itself as well.
The real difference in a practical migration: FileDocument was a value type (struct), so every read produced a new copy; on ReadableDocument, you're applying a snapshot to a reference-type, @Observable document object instead. Migration isn't "change the protocol signature" — it's moving the document model from value to reference.
The most concrete consequence of this shift shows up in the SwiftUI view layer. onmyway133's statement — "so SwiftUI does not recreate it on every change, and a TextEditor bound to a document property does not lose its model on each keystroke" — shows this directly: once ReadableDocument moves to a reference type, the document object's identity is preserved even when the view redraws, so a TextEditor bound to the document property doesn't lose its model on every keystroke.
Dimension | FileDocument (old) | ReadableDocument (new) |
|---|---|---|
Type nature | Value (struct) | Reference, @Observable |
Read responsibility | In one pass, inside init(configuration:) | Applying a snapshot via DocumentReader |
Recreation on change | New struct copy on every edit | Object identity preserved, not recreated |
Content type declaration | static var readableContentTypes | readableContentTypes, reader(configuration:), apply(snapshot:previous:) |
ReferenceFileDocument → WritableDocument
ReferenceFileDocument was already a reference type, so moving to WritableDocument is conceptually less of a leap than the ReadableDocument migration.
onmyway133's point brings clarity here: "You convert between your data and disk through a snapshot, using either the FileWrapperDocumentReader and FileWrapperDocumentWriter convenience types or a fully custom reader and writer for streaming or direct URL access." So on the WritableDocument side, the data conversion now happens through a snapshot, either via a concrete convenience type like FileWrapperDocumentWriter or through a fully custom reader/writer — conceptually corresponding to ReferenceFileDocument's snapshot(contentType:) + fileWrapper(snapshot:configuration:) pair, but with disk access now extended with streaming/direct URL support.
Feature | ReferenceFileDocument (old) | WritableDocument (new) |
|---|---|---|
Type nature | Reference (class), ObservableObject | Reference, @Observable |
Write helper type | fileWrapper(snapshot:configuration:) | FileWrapperDocumentWriter / DocumentWriter |
Disk access | Single-shot FileWrapper production | Streaming or direct URL support |
Progress reporting | Not specified | Subprogress parameter |
A concrete Writer type
WWDC26 session 269 gives a concrete writer type that conforms to DocumentWriter like this: struct Writer<Snapshot>: DocumentWriter { typealias Snapshot = PageSnapshot; let contentType: UTType }. So Writer is a generic helper type that knows which PageSnapshot to write for which contentType; the WritableDocument.writer(configuration:) method returns an instance of this type every time it's called.
Exporting the same document as PNG too
In the same session, the content types Writer supports aren't limited to a single format. Around 14:35, .png is added to the writableContentTypes list; at 14:48, the writer branches on it like this: if contentType.conforms(to: .stickerDocument) { /* write the .stickerDocument package */ } else if contentType.conforms(to: .png) { /* write the flattened image for .png */ }. So the same Writer, by checking the contentType parameter, can produce either the document's own package format (.stickerDocument) or a flattened PNG output — this is the branch that kicks in for exporting the page visually.
Incremental and asynchronous writing
Computing the diff with the previous parameter
The signature in the WWDC26 session's code example shows directly how incremental writing works: DocumentWriter.write(snapshot:to:previous:progress:). The previous parameter here carries the prior state into the writer; this lets the writer write only the part that changed, not the whole document.
1nonisolated func write(2 snapshot: sending PageSnapshot,3 to destination: URL,4 previous: sending PageSnapshot?,5 progress: consuming Subprogress6) async throws {7 // write .stickerDocument8 // previous == nil → first save, full write9 // previous != nil → only the diff is written (incremental write)10}Apple's session 269 walkthrough directly confirms this: the write compares current and previous snapshots and applies only to the package parts that actually need updating, running in the background without blocking the main thread — so incremental writing isn't just "write less data," it's also "don't keep the UI thread busy."
In practice: in a large document (say, a drawing file with dozens of pages), every Cmd+S now writes to disk only the parts that changed since the last operation, not the entire document.
Progress reporting with Subprogress
The progress: consuming Subprogress parameter you saw in the signature above is Foundation's progress-reporting API integrated directly into the SwiftUI Document layer. The WWDC26 guide summarizes it this way: "offer asynchronous, incremental disk operations and progress reporting via the Foundation Subprogress API." onmyway133 confirms the same point: "Packages can be read and written incrementally, and progress flows through Subprogress." — two independent sources agree here.
The consuming keyword shows that the Subprogress value is handed over (ownership transfer) to the write call — meaning the writer reports the progress of its own sub-operations back up to the caller through this single parameter. This means that when you want to show the user something like "42% saved" while saving a large document, you don't need to manually synchronize your own progress counters; the framework carries that for you.
What the consuming keyword means
1// The result from the signature (verified in WWDC26): consuming Subprogress2// "consuming" → the value is handed over to the write() call, ownership never returns.3// Practical effect: you don't need to update progress by hand,4// DocumentWriter reports its own sub-operations' progress through this parameter.DocumentCreationSource and NewDocumentButton
The WWDC26 guide defines this feature in one clear sentence: "The DocumentCreationSource API lets you declare multiple creation sources with a NewDocumentButton for each." So instead of one "New Document" button, an app can now define multiple buttons, each for a different starting state (blank page, template, import from photo, and so on).
The literal call from the WWDC26 session's code example is this:
1extension DocumentCreationSource {2 static let blank = Self(id: "blank")3 static let photo = Self(id: "photo")4}5 6DocumentGroupLaunchScene("Create a Sticker Page") {7 NewDocumentButton("New Sticker Page", source: .blank)8 NewDocumentButton("Sticker Page from Photo…", source: .photo)9}Adding multiple sources to the scene
The pattern is clear: each source is a static DocumentCreationSource value initialized with an id string, and NewDocumentButton takes a title string plus this value. The buttons' layout in the scene is just as clear: multiple NewDocumentButtons are defined one after another inside DocumentGroupLaunchScene, each carrying its own source. In practice: a drawing app with two flows like "Blank Page" and "Start from Photo" means two separate NewDocumentButton lines, each with a different source value.
iCloud/Files integration pitfalls
There's a broadly applicable pitfall to keep in mind when thinking about iCloud or Files syncing, confirmed in onmyway133's write-up: "One thing to remember is that SwiftUI tracks unsaved changes through undo actions, so without registered undo actions it will not autosave." In other words, SwiftUI tracks unsaved changes through undo actions. If a change hasn't been registered on the undo stack, autosave isn't triggered.
Why autosave doesn't trigger without a registered undo action
The practical consequence of this is: if you mutate your document by directly assigning to an @Observable property (without registering an undo action), SwiftUI doesn't mark that change as "unsaved" and won't autosave it — the file stays in its old state on disk (and therefore in iCloud sync too). That's why every mutation needs to go through a registered action via UndoManager; otherwise the change appears to the user as if it's "lost," when in fact it was never written at all.
iOS 26 backward compatibility
Apple's canonical documentation data is clear: the new protocols ship only with iOS 27.0, iPadOS 27.0, Mac Catalyst 27.0, macOS 27.0, and visionOS 27.0 (per the platform records in readabledocument.json and writabledocument.json) — not backported to iOS 26. The old FileDocument and ReferenceFileDocument have existed since iOS 14.0 / macOS 11.0; as of publish date, a mixed deployment target (one also covering below 27) has to stay on these protocols, since the new ones don't run there.
The consequence is this: if your deployment target is 27+, onmyway133's write-up gives clear advice — "If you are starting a document app today and your deployment target is 27 or later, reach for these rather than the older protocols." With a mixed deployment target (iOS 26+27), staying on the old API isn't a preference, it's a requirement — the new protocols don't exist on iOS 26.
What to do with a mixed deployment target
This advice needs to be read together with the onmyway133-sourced "replace ... for new code" statement from the "Why the new protocols arrived" section above: the new protocols replace the old ones for newly written code, but since the old ones are still usable as of publish date, an app with a mixed deployment target can host both protocol families at once — one for the 27+ flow, the other for backward compatibility.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
If you've read this far, you've earned an ordered checklist you can follow when moving to the new Document API. The items below gather the sourced pitfalls and steps from this post into a single sequence — check each one off before you start the migration.
FAQ
What replaces FileDocument now?
With iOS, macOS, and visionOS 27, Apple introduced the ReadableDocument and WritableDocument protocols for document-based SwiftUI apps. In onmyway133's words, these two protocols replace the old FileDocument/ReferenceFileDocument pair ("two new protocols replace FileDocument and ReferenceFileDocument for new code.") and are positioned as the recommended approach for new code.
What's the difference between ReadableDocument and WritableDocument?
ReadableDocument holds the list of supported content types and knows how to apply snapshots read from disk onto the document; this work is carried out via a DocumentReader. WritableDocument provides the list of writable formats and a snapshot mechanism that returns the current content, through a writer type that conforms to DocumentWriter — twin protocols, one carrying the reading responsibility, the other the saving responsibility.
How do I incrementally save a large file in SwiftUI?
WritableDocument's writer takes the prior state via previous in write(snapshot:to:previous:progress:) and writes only the changed part. The same call reports progress via progress: consuming Subprogress — so instead of rewriting large documents from scratch, you save them piece by piece, asynchronously.
How do you define multiple new-document sources in a DocumentGroup?
You define a DocumentCreationSource value for each different starting state and pass it to a NewDocumentButton("Title", source: source) call. The verified call from the WWDC26 code example looks like NewDocumentButton("New Sticker Page", source: .blank); you can reuse the same button type with different source values to offer multiple creation options inside a DocumentGroupLaunchScene.
What happens if I don't register an undo action?
Because SwiftUI tracks unsaved changes through the undo stack, autosave isn't triggered unless a mutation was made as an action registered with UndoManager. This can leave the document looking changed in the UI while staying in its old state on disk — which is why every edit needs to happen inside an undo-aware action.
Do I need to change my old FileDocument code right away?
Not in the short term. FileDocument and ReferenceFileDocument have been usable since iOS 14.0 / macOS 11.0. The new pair ships only with iOS/macOS/visionOS 27.0+, not backported to iOS 26. If your target also covers below 27, staying on the old API is required — the new protocols don't run there. See '## Update (September 2026)' below for a post-publish development on these protocols' status.
Update (September 2026)
After this post's publish date (June 19, 2026), a real change landed in Apple's canonical documentation data: FileDocument and ReferenceFileDocument were marked deprecated as of 27.2. The warning in FileDocument's records reads: "Conform your type to Document instead." ReferenceFileDocument's reads: "Use Document protocol instead." Both protocols had existed since iOS/iPadOS/Mac Catalyst 14.0, macOS 11.0; the deprecation only arrived with 27.2, and isn't a retroactive behavior change.
The Document protocol Apple points to inherits both ReadableDocument and WritableDocument: "Inherits From: ReadableDocument, WritableDocument", with the abstract "A document that supports both reading and writing." (iOS/macOS/visionOS 27.0). So the twin-protocol model this post describes hasn't changed; Document was added on top as a third protocol unifying the two under one name. Read this together with the earlier '## iOS 26 backward compatibility' section: if your deployment target also covers below 27, staying on the old protocols is still required — but it's now "deprecated, yet currently the only option," not "not deprecated."
Conclusion
ReadableDocument and WritableDocument are among the most concrete infrastructure updates SwiftUI has shipped for document-based apps: a reference-type, @Observable document model, incremental/asynchronous writing that reports progress via Subprogress, and multiple document-creation sources via DocumentCreationSource. But this doesn't have to be a "big bang" migration: the new protocols replace the old ones for newly written code, but FileDocument/ReferenceFileDocument are still required for deployment targets that cover below iOS 27; migrating only becomes a choice once your deployment target is 27+ (see the Update section above).
To refresh SwiftUI's state management before moving your document model to a reference type, see my deep dive into SwiftUI property wrappers. Planning a document-based macOS app? The guide to desktop development with SwiftUI on macOS is a natural next step. Curious about other iOS 27 breaking changes? See the post on what broke in the State macro and ContentBuilder. If you're splitting your document model into components, the custom component library guide and, for large-document performance, the performance optimization post are useful references too.
Sources
- WWDC26 SwiftUI guide — Apple's official guide; the primary source for ReadableDocument/WritableDocument, Subprogress, and DocumentCreationSource.
- What's new in SwiftUI in iOS 27 (onmyway133) — an independent write-up dated June 9, 2026; a second source confirming most items, including the undo-based autosave pitfall.
- WWDC26 "What's new in SwiftUI" session video — the session where the DocumentWriter signature and the NewDocumentButton code example appear.
- ReadableDocument documentation — the protocol's official reference page.
- WritableDocument documentation — the protocol's official reference page.
- FileDocument documentation — the old protocol's canonical reference page.
- ReferenceFileDocument documentation — the old reference-type protocol's canonical page.
- Document protocol documentation — the recommended replacement for FileDocument/ReferenceFileDocument, inheriting ReadableDocument+WritableDocument.
- DocumentGroup documentation — the canonical type DocumentCreationSource and NewDocumentButton attach to.
- Foundation Subprogress documentation — the Foundation API used for progress reporting.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

