SwiftUI DocumentGroup: Build Document-Based Apps for iPhone, iPad, Mac, and visionOS
A practical guide to SwiftUI DocumentGroup in iOS 26: FileDocument vs ReferenceFileDocument, custom UTType setup, FileWrapper for packages, autosave and undo wiring, plus the platform quirks on iPhone, iPad, Mac, and visionOS that nobody warns you about.
SwiftUI DocumentGroup is a scene type that gives you a fully-featured document-based app (open, save, rename, duplicate, version, autosave) across iPhone, iPad, Mac, and visionOS with a single declaration that wraps your FileDocument or ReferenceFileDocument conformance. In iOS 26 it gained a proper document browser on iPhone, better window scene plumbing on visionOS, and a much-needed DocumentGroupLaunchScene for custom launch experiences. I've shipped a notes app that runs on all four platforms from the same target, and honestly, the differences between how this scene behaves on each one are the kind of thing nobody tells you up front.
DocumentGroup handles file lifecycle, the document browser, the recents list, and iCloud Drive integration automatically. You only provide the model and a view.
Use FileDocument for value-type documents and ReferenceFileDocument when you need an ObservableObject with undo manager integration.
Declare custom file types in your target's Document Types and Exported Type Identifiers Info settings, then mirror them with a UTType extension.
The same DocumentGroup renders as a fullscreen browser on iPhone, a sidebar-aware Files-style picker on iPad, a standard NSDocument-backed window on Mac, and a floating window on visionOS.
iOS 26 adds DocumentGroupLaunchScene for branded launch screens, and FileDocumentConfiguration exposes a binding to the document plus the file URL.
Autosave is automatic. For binary or package formats, use FileWrapper instead of Data in your snapshot.
What is SwiftUI DocumentGroup?
DocumentGroup is a SwiftUI Scene introduced in iOS 14 / macOS 11 that brings the full document-based app experience (the same one UIDocumentBrowserViewController and NSDocument have offered for years) into a declarative app lifecycle. You declare it once in your App body, hand it a document type and an editor view, and the system handles the open/save/rename/duplicate UI, the recents list, iCloud Drive integration, Files.app entry points, autosave, version browsing, and the share sheet plumbing.
The win isn't just less code. It's that the same scene declaration produces platform-idiomatic chrome on each Apple platform. On iPhone you get a fullscreen browser that matches Files.app. On iPad you get the multi-column browser with sidebar. On Mac you get a real NSDocument-backed window with the standard File menu items wired up. On visionOS you get a floating window with a translucent toolbar. I've found this is the single biggest reason to use DocumentGroup over rolling your own picker plus storage: you stop fighting platform conventions and your app just feels right everywhere.
In iOS 26 the scene picked up a few important additions: DocumentGroupLaunchScene for branded first-launch experiences, an iPhone-specific browser that respects the new Liquid Glass toolbar treatment, and improved scene restoration when documents are open on visionOS across multiple windows. Apple's DocumentGroup reference is the canonical source, but it stops short of explaining the per-platform differences that actually matter in practice.
FileDocument vs ReferenceFileDocument: which should you pick?
This is the first decision you make, and it's worth a minute. FileDocument is a value type (typically a struct), and SwiftUI hands you a binding to it inside the editor view. Saves happen by snapshotting the value and writing it. ReferenceFileDocument is a class (an ObservableObject) and saves happen via a two-phase snapshot: you produce an immutable snapshot synchronously, then SwiftUI writes it on a background queue so the user can keep editing.
Pick FileDocument when your model is small to medium, can be copied cheaply, and doesn't need to participate in undo. Markdown editors, JSON config editors, plain text apps, and most schema-driven document formats all fit.
Pick ReferenceFileDocument when any of these are true: your model is large enough that copying it on every save would stutter the UI; you need UndoManager integration for a real Edit menu with multi-step undo; you have child objects that need stable identity across saves (think a drawing app with shape objects). The two-phase snapshot is the killer feature here. You can keep typing in a rich text editor while the previous state writes out.
A useful rule of thumb: if your document, fully expanded, fits comfortably in a few hundred kilobytes, start with FileDocument. You can always migrate later. The editor view shape barely changes.
A minimum viable document-based app
So, let's build one. This is a working text-document app that compiles on Xcode 26 and runs on iOS 26, macOS 26 (Tahoe), and visionOS 26. The whole thing is three files.
// TextDocument.swift
import SwiftUI
import UniformTypeIdentifiers
struct TextDocument: FileDocument {
// The types this document reads. Drives the Files.app filter and the Open panel.
static var readableContentTypes: [UTType] { [.plainText] }
// Optional. Defaults to readableContentTypes. Override if you write a subset.
static var writableContentTypes: [UTType] { [.plainText] }
var text: String
init(text: String = "") {
self.text = text
}
// Called when the system opens an existing file.
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8) else {
throw CocoaError(.fileReadCorruptFile)
}
self.text = string
}
// Called when the system needs to write (autosave, explicit save, version snapshot).
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
// ContentView.swift
import SwiftUI
struct ContentView: View {
// A Binding into the document. Mutations are automatically dirtied for autosave.
@Binding var document: TextDocument
var body: some View {
TextEditor(text: $document.text)
.font(.system(.body, design: .monospaced))
.padding(8)
}
}
// MyNotesApp.swift
import SwiftUI
@main
struct MyNotesApp: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
ContentView(document: file.$document)
}
}
}
That's a complete cross-platform document app. Run it on iPhone, iPad, Mac, or visionOS, and you get a working browser, new-document button, rename, duplicate, autosave, iCloud Drive sync, and on Mac a real File → Save menu without you writing a single line of menu code.
How do you add custom file types in SwiftUI?
Plain text is fine for a demo, but real apps usually have their own file type. There are three places you need to wire one up: the Info plist (or modern Info settings tab in Xcode 26), a UTType extension, and your document's readableContentTypes.
In Xcode 26, open your target's Info tab. Add an entry to Exported Type Identifiers with these fields:
Description: "My Notes Document"
Identifier: dev.swiftcrafted.mynotes (reverse-DNS, must be unique to you)
Conforms To: public.data for opaque binaries, or public.plain-text if it's a text subtype
Extensions: mynote
MIME Types: application/x-mynotes (optional but recommended for share extensions)
Then add an entry to Document Types referencing that identifier and pick the document role (Editor vs Viewer). Now mirror it in Swift:
This is the section I wish someone had handed me before I shipped my first document-based app. The declaration is identical; the runtime is not.
Aspect
iPhone (iOS 26)
iPad (iPadOS 26)
Mac (macOS 26)
visionOS 26
Browser UI
Fullscreen, Files-like
Multi-column with sidebar
NSOpenPanel + recents
Floating window, single column
Multiple documents open
One at a time
Multitasking, multiple windows
Native multi-window
One window per document
File menu items
n/a
n/a
Full File menu wired up
n/a
Autosave
On backgrounding
On backgrounding + idle
On idle + window close
On backgrounding
Versions / version browser
No
No
Yes (Revert To)
No
External keyboard shortcuts
If attached
Yes (Cmd-N, Cmd-S)
Yes
If attached
iCloud Drive default
Yes
Yes
Yes
Yes
Two practical consequences. First, never assume your app has a File menu. On iPhone and visionOS it doesn't, so anything you want users to do (export, print, share) has to live in the editor's toolbar. The good news: the same toolbar code works everywhere if you build it once. Second, on Mac your document can be reverted to a prior version via File → Revert To; on every other platform that affordance simply doesn't exist. Don't gate critical features on it.
If your app also needs multi-pane navigation inside a document, see my SwiftUI NavigationSplitView guide, which composes inside the editor view returned by DocumentGroup just fine.
FileWrapper and package file formats
The simple example above used FileWrapper(regularFileContents:) for a single flat blob. Real document formats often need more: a Pages document is a package (a folder that looks like a file) containing XML, images, and previews. A drawing app might want to embed image attachments. For these, return a directory FileWrapper:
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let manifestData = try JSONEncoder().encode(manifest)
let manifestWrapper = FileWrapper(regularFileWithContents: manifestData)
manifestWrapper.preferredFilename = "document.json"
var children: [String: FileWrapper] = ["document.json": manifestWrapper]
for (name, imageData) in attachments {
children["attachments/\(name)"] = FileWrapper(regularFileWithContents: imageData)
}
return FileWrapper(directoryWithFileWrappers: children)
}
For package types, set the UTType to conform to com.apple.package in the Info settings. Finder, Files.app, and iCloud Drive will then treat the folder as an opaque document and never let users wander inside it.
If your document embeds rich text, consider pairing FileWrapper with the new AttributedString codable encoding. I wrote about that in the SwiftUI rich text editor guide.
Autosave, undo, and versioning
Autosave is on by default. Every mutation through the binding marks the document dirty; SwiftUI flushes on app backgrounding (iOS, visionOS) or after an idle interval and on window close (Mac). You don't call save explicitly. There is no save button. If you find yourself wanting one, you've probably misunderstood the document lifecycle. It's the platform that owns the file now, not you.
Undo integration differs by document type. FileDocument gives you basic state diffing. SwiftUI undoes the binding mutation, but it has no idea what semantic operation that mutation represented ("delete paragraph" vs "type 'a'"). For real multi-level semantic undo you need ReferenceFileDocument and a UndoManager wired through:
final class DrawingDocument: ReferenceFileDocument {
typealias Snapshot = DrawingState
@Published var state: DrawingState
static var readableContentTypes: [UTType] { [.myDrawing] }
init() { self.state = DrawingState() }
required init(configuration: ReadConfiguration) throws {
let data = configuration.file.regularFileContents ?? Data()
self.state = try JSONDecoder().decode(DrawingState.self, from: data)
}
func snapshot(contentType: UTType) throws -> DrawingState {
return state // immutable copy taken synchronously
}
func fileWrapper(snapshot: DrawingState, configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(snapshot)
return FileWrapper(regularFileWithContents: data)
}
func addShape(_ shape: Shape, undoManager: UndoManager?) {
state.shapes.append(shape)
undoManager?.registerUndo(withTarget: self) { doc in
doc.removeShape(shape, undoManager: undoManager)
}
}
func removeShape(_ shape: Shape, undoManager: UndoManager?) {
state.shapes.removeAll { $0.id == shape.id }
undoManager?.registerUndo(withTarget: self) { doc in
doc.addShape(shape, undoManager: undoManager)
}
}
}
Grab the UndoManager with @Environment(\.undoManager) inside the editor view and pass it to mutation methods. The Mac gets a proper Edit menu wiring automatically; on iPad you'll need to surface it via a toolbar button or the .keyboardShortcut modifier.
DocumentGroupLaunchScene in iOS 26
Before iOS 26 the first-launch experience for a document app was utilitarian, basically a generic browser with your app icon. iOS 26 adds DocumentGroupLaunchScene, which lets you provide a branded splash with custom artwork, a list of suggested templates, and entry points to existing documents.
A quick tour of the rakes I've stepped on, so you don't have to:
"My custom file extension doesn't open anything." Almost always the UTType identifier in code doesn't match Info. Open the Info settings, copy the exact string, paste into UTType(exportedAs:).
"Files.app doesn't show my app under 'Open in'." Your document role is set to Viewer instead of Editor, or your UTType doesn't declare a conformance to public.data.
"The Mac Save panel won't let me type my extension."writableContentTypes is missing or doesn't include your custom type. Set it explicitly even if it duplicates readableContentTypes.
"Autosave is creating empty files when I quit fast." You're mutating outside the binding. For example, holding your own @State copy and writing it back on a timer. Always mutate through $document so SwiftUI marks the document dirty correctly.
"On visionOS the document window is the wrong size." Apply .defaultSize(width: 800, height: 600) to the DocumentGroup. Without it, visionOS uses a tiny default.
"My iCloud Drive folder doesn't appear." Add the iCloud capability with the Documents service enabled, and ensure your container identifier matches your bundle ID prefix. The Apple iCloud documents guide walks through the entitlement.
And one I see in every code review: people gate features on fileURL == nil thinking "new, unsaved document." That works on first launch but breaks on iPad multitasking where the file URL is set as soon as the user picks a name. Gate on isEditable instead if you mean "can the user actually change this," and on a custom "has user typed anything" flag if you mean unsaved.
When you should not reach for DocumentGroup
Pragmatically, not every app with files needs DocumentGroup. If your app's mental model is a list of records the user creates inside the app, with no notion of files in the user's filesystem, you want SwiftData or Core Data, not a document app. A todo app is not a document app. A workout tracker is not a document app. Reaching for DocumentGroup here gets you a browser the user doesn't want and a Files.app entry that doesn't make sense. The SwiftData framework reference is the better starting point for that shape of app.
Use DocumentGroup when (a) the user's mental model is "I have files," (b) you want the user to be able to share, move, back up, or sync those files independently of your app, and (c) you're OK with the platform owning the file lifecycle. If any of those is false, look at WindowGroup with SwiftData or Core Data underneath.
Frequently Asked Questions
Does DocumentGroup work on visionOS?
Yes, since visionOS 1.0. Each open document becomes its own floating window, and the document browser appears in a single-column layout. The biggest gotcha is window sizing: apply .defaultSize(width:height:) to the DocumentGroup, because the platform's default size is too small for most editors.
Can you use DocumentGroup with Mac Catalyst?
You can, but in 2026 you usually shouldn't. A native macOS target compiled from the same SwiftUI source produces a much better File menu, version browser, and Open panel experience than the Catalyst bridge does. If you're already on Catalyst, DocumentGroup works — just expect a few rough edges around the File menu items that appear under the wrong section.
How do you handle autosave with DocumentGroup?
You don't, in the active sense — it's automatic. SwiftUI marks the document dirty whenever you mutate through the binding (FileDocument) or call objectWillChange.send() (ReferenceFileDocument), and writes happen on idle, backgrounding, or window close. If autosave seems to be missing changes, the cause is almost always that you're mutating state that isn't reachable from the binding.
Can a SwiftUI app have multiple DocumentGroup scenes?
Yes, declare one per document type. An app that edits both .mynote and .mynotebook files would declare two DocumentGroup scenes side by side in body. SwiftUI routes each opened file to the matching scene based on its UTType.
How do you open a document programmatically?
Use the NewDocumentAction or OpenDocumentAction environment values: @Environment(\.newDocument) and @Environment(\.openDocument). Call openDocument(at: url) from within a button to open a specific URL. Both are available across iOS, macOS, and visionOS.
Build production-ready custom SwiftUI layouts in iOS 26. Walk through ProposedViewSize, layout caches, LayoutValueKey, and animating between layouts with AnyLayout.
NavigationSplitView is SwiftUI's adaptive sidebar container. Learn selection bindings, deep linking, iPhone collapse behaviour, and accessibility patterns for iOS 26.
Master SwiftUI ScrollView in iOS 26 with scrollPosition, scrollTransition, scrollTargetBehavior, contentMargins, paging, and Liquid Glass edge effects, all with runnable Xcode 26 code.