---
title: Handling advanced document scenarios
framework: swiftui
role: article
role_heading: Article
path: swiftui/handling-advanced-document-scenarios
---

# Handling advanced document scenarios

Extend your document-based app to support custom file formats, on-demand file access, and progress reporting.

## Overview

Overview After you’ve created a working document-based app, you can extend it to handle scenarios that go beyond basic reading and writing. Support file formats that the system doesn’t know about by default by using a custom UTType. Access files outside the normal read and write lifecycle using makeFileCoordinator(), for example, to open a specific subfile in a package on demand. Add progress reporting to give people feedback during long operations. note: If you’re new to document-based apps in SwiftUI, start with Creating a document-based app. Declare a custom file format The system knows about popular built-in formats and their identifiers, like utf8PlainText, jpeg, and markdown. For your own file formats, declare a custom UTType so the Finder, the document browser, and Spotlight understand them. Every custom type needs a base type that tells the system what kind of file it is. Use public.data or conforming types as a base type for single-file documents, or com.apple.package for documents the system stores as a directory but presents as a single file. Declare the content type in your app target’s information property list under UTExportedTypeDeclarations so the Finder, the document browser, and Spotlight all recognize it. Set UTTypeConformsTo to the matching base type and add a UTTypeTagSpecification that maps the type to your filename extension. The example below declares a notebook package; for a flat-file format, swap com.apple.package for public.data, like this: <key>UTExportedTypeDeclarations</key> <array>     <dict>         <key>UTTypeIdentifier</key>         <string>com.example.notebook</string>         <key>UTTypeConformsTo</key>         <array>             <string>com.apple.package</string>         </array>         <key>UTTypeTagSpecification</key>         <dict>             <key>public.filename-extension</key>             <array>                 <string>examplenotebook</string>             </array>         </dict>     </dict> </array> For convenience, mirror the declaration in code, as shown here: extension UTType {     static let notebook = UTType(exportedAs: "com.example.notebook") } Then reference your type from the document’s readableContentTypes and writableContentTypes, like this: static let readableContentTypes: [UTType] = [.notebook] static let writableContentTypes: [UTType] = [.notebook, .utf8PlainText] For more about declaring uniform type identifiers for proprietary formats, see Defining file and data types for your app. note: Every Mac registers the standard content types that the system provides, but the set of less common types each computer recognizes depends on the software installed on it. A computer might not recognize certain media types, for example, if no installed app declares them. Similarly, a computer that doesn’t have Xcode installed doesn’t recognize com.apple.xcode.resultbundle, the type identifier for Xcode result bundles. Access files outside read and write SwiftUI coordinates file access for read and write calls automatically. To access the file URL at other times — for example, when reading a specific subfile in a package — use the configuration’s file coordinator, as shown here: let coordinator = document.configuration.makeFileCoordinator() coordinator.coordinate(     readingItemAt: packageURL.appending(path: "metadata.json"),                        options: []) { url in     do {         let data = try Data(contentsOf: url)         let metadata = try JSONDecoder().decode(NotebookMetadata.self, from: data)         // Process the metadata.     } catch {         // Handle errors.     } } important: Always use makeFileCoordinator() for disk access outside of read and write. Accessing the file URL directly risks corruption because other processes, including iCloud, can change it any time. Report progress Packages let you read and write incrementally; instead of loading or saving the whole document on every change, you can read only the files you need and write only the ones that changed. For notebook documents that consist of multiple files, custom image formats that store data in separate files, or projects with embedded media, this can mean the difference between a fast autosave and a slow one. Both DocumentReader and DocumentWriter receive a Subprogress parameter via their read and write methods, respectively. Report progress through the parameter so SwiftUI can display the appropriate UI during long operations. Create a ProgressReporter from the Subprogress by specifying a total unit count. Then call complete(count:) as the work finishes, like this: @concurrent  func read(from source: URL, progress: consuming Subprogress) async throws -> sending ImageSnapshot {     let progressManager = progress.start(totalCount: 2)     let data = try Data(contentsOf: source)     progressManager.complete(count: 1)     let image = try decodeImage(from: data)     progressManager.complete(count: 1)     return ImageSnapshot(image: image) } For package documents, you can treat each file as an equal chunk of work, as shown below. This approach gives granular feedback as the writer processes files one by one. @concurrent func write(     snapshot: sending NotebookSnapshot, to destination: URL,     previous: sending NotebookSnapshot?, progress: consuming Subprogress ) async throws {     let changedPages = snapshot.pages.filter { (identifier, content) in         previous?.pages[identifier] != content     }

// One unit for metadata, and one unit per changed page.     let totalUnits = 1 + changedPages.count     let progressManager = progress.start(totalCount: totalUnits)

// Write the metadata.     let metadataURL = destination.appending(path: "metadata.json")     let metadataData = try JSONEncoder().encode(snapshot.metadata)     try metadataData.write(to: metadataURL, options: .atomic)     progressManager.complete(count: 1)

// Write each changed page individually.     let fileManager = FileManager.default     let pagesDirectory = destination.appending(path: "pages")

// Create the pages subdirectory if it doesn't already exist.     try? fileManager.createDirectory(         at: pagesDirectory, withIntermediateDirectories: true     )

for (identifier, content) in changedPages {         let pageURL = pagesDirectory.appending(             path: "\(identifier.uuidString).txt"         )         let data = Data(content.utf8)         try data.write(to: pageURL, options: .atomic)         progressManager.complete(count: 1)     } } If you use FileHandle or OutputStream to write files in chunks — for example, a large media file — report progress after each chunk rather than only once at the end. Each call to complete(count:) moves the progress bar forward, so spread the calls throughout the write. The example below writes a large media file to disk in chunks. Set the total unit count to the file size in bytes, and call the reporter after each chunk so the progress bar updates smoothly. @concurrent func write(     snapshot: sending MediaSnapshot, to destination: URL,     previous: sending MediaSnapshot?, progress: consuming Subprogress ) async throws {     let payload = snapshot.payload     let totalBytes = payload.count     let progressManager = progress.start(totalCount: totalBytes)

try Data().write(to: destination)     let fileHandle = try FileHandle(forWritingTo: destination)     defer { try? fileHandle.close() }

// Aim for ~100 progress updates across the write, clamped so chunks     // stay large enough to amortize system call overhead and small enough     // to keep the progress bar moving on tiny payloads.     let targetUpdateCount = 100     let minimumChunkSize = 64 * 1024        //  64 KB     let maximumChunkSize = 4 * 1024 * 1024  //   4 MB     let chunkSize = min(         maximumChunkSize,         max(minimumChunkSize, totalBytes / targetUpdateCount)     )

var offset = 0     while offset < totalBytes {         let end = min(offset + chunkSize, totalBytes)         let chunk = payload[offset..<end]         try fileHandle.write(contentsOf: chunk)         progressManager.complete(count: end - offset)         offset = end     } } The chunk size adapts to the file size, so large files don’t generate unnecessary system overhead. If you stream to an OutputStream instead, the same pattern applies: Open the stream, write chunks of the computed size, call complete(count:) with the bytes written, and call ProgresManager.complete(count:) when finished. note: Even when you report progress, SwiftUI decides whether to show a progress view.

## See Also

### Creating a document

- [Creating a document-based app](swiftui/creating-a-document-based-app.md)
- [Updating your document-based app](swiftui/updating-your-document-based-app.md)
- [Building a document-based app with SwiftUI](swiftui/building-a-document-based-app-with-swiftui.md)
- [Building a document-based app using SwiftData](swiftui/building-a-document-based-app-using-swiftdata.md)
- [DocumentGroup](swiftui/documentgroup.md)
