SwiftUI’s document architecture has quietly become one of the most powerful foundations for building professional macOS, iPadOS, and iOS apps. Apps such as Xcode, Pages, Pixelmator Pro, and many creative tools rely on document-based workflows that provide autosaving, versioning, keyboard shortcuts, window management, and seamless integration with the file system.
In the 2027 releases, Apple significantly expands the SwiftUI document APIs with a more modern, performance-focused architecture.
The update introduces three major improvements:
FileDocument and ReferenceFileDocument document architecture.These APIs make it easier to build document editors that feel native, remain responsive while handling large files, and provide richer creation experiences than the traditional “new blank document” flow.
In this article, we’ll explore the complete document architecture, build a real document-based app, and understand how the new APIs improve both performance and user experience.
A SwiftUI document app automatically gains a surprising amount of functionality.
When you use DocumentGroup, the system provides:
The new APIs build on this foundation rather than replacing it.
The result is a cleaner separation between document data, disk operations, and user interface state.
A document-based app begins with DocumentGroup.
@main
struct SketchApp: App {
var body: some Scene {
DocumentGroup {
SketchDocument()
}
WindowGroup {
SettingsView()
}
}
}DocumentGroup becomes the primary scene responsible for opening, creating, and managing documents.
Your document type becomes the central source of truth for the application.
The document architecture now works particularly well with the Observation framework.
import Observation
@Observable
final class SketchDocument {
var canvasSize: CGSize = .init(width: 1024, height: 1024)
var strokes: [Stroke] = []
var backgroundImage: URL?
}Using @Observable means SwiftUI updates only the views that actually depend on modified properties.
For large documents containing thousands of objects, this can significantly reduce unnecessary view updates.
One of the most visible improvements is the ability to create documents from different starting points.
Consider a drawing app.
Previously, pressing New always created the same document.
Now you can offer multiple creation experiences:
The new DocumentCreationSource API makes this straightforward.
enum SketchCreationSource: String, Codable {
case blank
case photo
case template
}DocumentGroup(
newDocument: SketchDocument.init
) {
EditorView(document: $0.document)
} launchScene: {
NewDocumentButton("Blank Canvas")
NewDocumentButton(
"From Photo",
source: SketchCreationSource.photo
)
NewDocumentButton(
"Template",
source: SketchCreationSource.template
)
}Each button declares a different document creation source.
When the user selects a button, SwiftUI passes the selected source into the document creation context.
The document initializer receives contextual information about how the document was created.
init(context: DocumentCreationContext) {
if let source = context.creationSource(
SketchCreationSource.self
) {
switch source {
case .blank:
break
case .photo:
showingPhotoPicker = true
case .template:
loadDefaultTemplate()
}
}
}This enables experiences that previously required custom launch windows and complicated routing logic.
For example, selecting From Photo can immediately present the photo picker.
The user is only one tap away from editing a photo.
This creates a much more polished onboarding experience.
Earlier document APIs tended to combine multiple responsibilities into a single type.
The new architecture separates them into distinct protocols.
| Responsibility | Protocol |
|---|---|
| Reading documents | ReadableDocument |
| Writing documents | WritableDocument |
| Disk reading implementation | DocumentReader |
| Disk writing implementation | DocumentWriter |
This separation improves testability, concurrency, and performance.
Let’s make our document writable.
import UniformTypeIdentifiers
extension SketchDocument: WritableDocument {
static let writableContentTypes: [UTType] = [
.sketchDocument
]
}The protocol requires three main pieces:
The snapshot is one of the most important architectural changes.
Instead of writing the live document directly to disk, you create an immutable representation of the document.
struct SketchSnapshot: Sendable {
var canvasSize: CGSize
var strokes: [Stroke]
var backgroundImage: URL?
}The document provides a snapshot.
@MainActor
func snapshot(contentType: UTType)
async throws -> sending SketchSnapshot {
SketchSnapshot(
canvasSize: canvasSize,
strokes: strokes,
backgroundImage: backgroundImage
)
}This captures the document at a single point in time.
The writer can safely use this snapshot in the background.
The writer handles all disk operations.
struct SketchWriter: DocumentWriter {
typealias Snapshot = SketchSnapshot
let contentType: UTType
}The core method is write.
nonisolated
func write(
snapshot: sending SketchSnapshot,
to destination: URL,
previous: sending SketchSnapshot?,
progress: consuming Subprogress
) async throws {
// Write document package
}Several details here are worth discussing.
The write method is nonisolated.
This means it is not bound to the main actor.
Expensive work such as:
can occur in the background.
The UI remains responsive while saving.
For large creative documents, this is a major improvement.
Notice the previous snapshot parameter.
previous: sending SketchSnapshot?This allows incremental saving.
Instead of rewriting the entire document package every time, you can compare snapshots.
if snapshot.backgroundImage != previous?.backgroundImage {
writeBackgroundImage()
}
if snapshot.strokes != previous?.strokes {
writeStrokeData()
}For package-based formats, this can dramatically reduce disk activity.
Large saves should provide feedback.
SwiftUI integrates with Foundation’s progress reporting.
progress.complete(count: 1, of: 3)
writeMetadata()
progress.complete(count: 2, of: 3)
writeImages()
progress.complete(count: 3, of: 3)
writeThumbnail()This allows the system to display accurate save progress when needed.
Reading follows the same architecture.
extension SketchDocument: ReadableDocument {
static let readableContentTypes: [UTType] = [
.sketchDocument
]
}A document reader performs the heavy disk work.
The document then applies the loaded snapshot.
Conceptually, the flow becomes:
Disk
↓
DocumentReader
↓
Snapshot
↓
SketchDocument
↓
SwiftUI ViewsThis keeps disk operations isolated from UI state.
One of the most powerful aspects of WritableDocument is support for multiple output formats.
Suppose the app can export both its native document format and PNG images.
static let writableContentTypes: [UTType] = [
.sketchDocument,
.png
]The writer can branch based on the requested content type.
nonisolated
func write(
snapshot: sending SketchSnapshot,
to destination: URL,
previous: sending SketchSnapshot?,
progress: consuming Subprogress
) async throws {
if contentType.conforms(to: .sketchDocument) {
writeNativeDocument(snapshot)
}
else if contentType.conforms(to: .png) {
writePNG(snapshot)
}
}The PNG export can render the canvas using Core Graphics.
let context = CGContext(...)
drawBackground(snapshot, in: context)
drawStrokes(snapshot, in: context)
context.writePNG(to: destination)The same document can now support:
without separate export controllers.
These APIs are particularly valuable for large documents.
Imagine a document containing:
The old approach often involved:
The new architecture enables:
The writer runs asynchronously.
Serialization never races with live editing.
Only changed resources need rewriting.
Views update only when accessed properties change.
The combination substantially improves responsiveness.
Another important addition is first-class direct document URL access.
Document-based apps often need the document’s actual location.
Common scenarios include:
The expanded document APIs provide direct access to the document URL through the document architecture.
This removes much of the bookkeeping that previously required environment objects, window state tracking, or custom file coordination.
For package-based document formats, direct URL access makes it much easier to work with internal resources.
SwiftUI continues to support both document styles.
Use value semantics.
Best for:
Example:
struct NotesDocument: FileDocument {
var text: String
}Use reference semantics.
Best for:
Example:
@Observable
final class SketchDocument: ReferenceFileDocument {
var strokes: [Stroke] = []
}For most modern creative apps, ReferenceFileDocument is usually the better fit.
The new architecture creates a remarkably clean document lifecycle.
New Document Button
↓
DocumentCreationSource
↓
DocumentCreationContext
↓
SketchDocument
↓
Observable state
↓
SwiftUI views
↓
Snapshot
↓
DocumentWriter
↓
DiskReading follows the reverse direction.
The document remains focused on application state.
Readers and writers focus on file system operations.
Snapshots provide safe concurrency boundaries.
For existing document-based apps, migration can be incremental.
Start by adopting @Observable.
Then separate reading and writing logic.
Introduce snapshots.
Finally, move expensive serialization into DocumentWriter.
A practical migration path is:
@Observable.DocumentWriter.DocumentReader.DocumentCreationSource for richer creation flows.Each step provides independent benefits.
The expanded SwiftUI document APIs are much more than a small refinement.
They modernize the entire document architecture around Observation, Swift Concurrency, and asynchronous disk operations.
The biggest improvement is the clear separation between document state and file system work.
DocumentCreationSource enables creation experiences that feel much more native and intentional.
ReadableDocument and WritableDocument make large-file performance significantly better through snapshots and background readers and writers.
Direct document URL access simplifies integration with the broader file system and package-based document formats.
If you’re building a professional editor, drawing app, writing app, or any tool that works with files, these APIs make SwiftUI document-based apps substantially more capable, scalable, and maintainable than previous releases.
Thank you for reading. If you have any questions or opinion feel free to follow me on X and send me a DM. If this article helped you, Buy me a coffee.