Document-based apps often begin with serialization as an implementation detail. That works for a small text file, but becomes limiting when a document contains media, a package hierarchy, export formats, or enough data that writing can interrupt the interface.
The new SwiftUI Document APIs give reference-type document models direct access to file URLs, concurrency-aware reading and writing, and progress reporting. The important architectural change is a clean boundary: the editable document state stays close to the UI, while file work receives a stable snapshot and runs independently.
Choose the document model that matches the file
FileDocument remains a reasonable fit for simple, value-style serialization through FileWrapper. For a larger reference-type document, the newer ReadableDocument and WritableDocument protocols provide more control. A type that needs both can conform to Document.
That distinction matters when the document is a package rather than a single blob. A drawing document might contain original images, thumbnails, metadata, and export output. Direct disk access lets the reader and writer handle that structure without first forcing every byte into one in-memory representation.
The new APIs are currently marked beta. Keep their use behind a focused document layer and confirm the final signatures when adopting a release candidate.
Give creation flows a real source
A document launch scene can now distinguish between the ways a person starts a document. Instead of showing the same empty canvas for every path, an app can declare creation sources such as a blank document and a document that starts from a photo.
@main
struct ScrapbookApp: App {
var body: some Scene {
DocumentGroupLaunchScene("Create a page") {
NewDocumentButton("Blank page", source: .blank)
NewDocumentButton("Page from photo", source: .photo)
}
DocumentGroup { document in
PageEditor(document)
} { configuration, context in
PageDocument(configuration: configuration, context: context)
}
}
}
extension DocumentCreationSource {
static let blank = Self(id: "blank")
static let photo = Self(id: "photo")
}The document initializer can use its creation context to choose an appropriate first state, such as presenting a photo picker for the photo source. This keeps the decision in the creation path rather than adding special-case state to the editor after it appears.
Save a snapshot, not the live editor state
WritableDocument separates three responsibilities: declare the content types, produce a snapshot, and return a DocumentWriter that writes the snapshot. The snapshot is the handoff between UI-owned state and disk work.
@Observable
final class PageDocument: WritableDocument {
static let writableContentTypes: [UTType] = [.scrapbookPage]
@MainActor
func snapshot(contentType: UTType) async throws -> PageSnapshot {
PageSnapshot(items: items, background: background)
}
func writer(configuration: sending WriteConfiguration) -> PageWriter {
PageWriter(contentType: configuration.contentType)
}
}The writer's write method is asynchronous and nonisolated. That makes it the right place for expensive encoding, file copying, and package updates without blocking gestures or view updates. It also receives the previous snapshot, which allows a package writer to update only the assets that changed rather than rewrite every file on each save.
Report progress for work that a person may notice, especially an export with large media. A visible save state is more useful than a frozen editor with no explanation.
Make reading and exporting symmetrical
ReadableDocument pairs with a DocumentReader in the same way that writing pairs with DocumentWriter. Model the formats the app can read and write explicitly. A document editor can support its native package for continued editing and a flattened PNG or PDF for sharing, without treating export as a second document architecture.
Keep file format routing inside the reader or writer. The view should request an export, not decide which directory is written or how pixels are encoded. This separation also makes it much easier to test a corrupted package, a missing image, or an interrupted export without instantiating the entire UI.
The new architecture is not automatically faster because it uses new protocol names. It is faster when the snapshot is compact, writing is off the main actor, and the writer can avoid touching unchanged files. That is the difference between a document feature that saves data and one that continues to feel responsive as its files become real user work.
