How to implement a document provider in SwiftUI
A Document Provider has two parts: a File Provider Extension target (conforming to NSFileProviderReplicatedExtension) that exposes your files to the system, and a SwiftUI wrapper around UIDocumentPickerViewController that lets users open those files in-app. Both targets share data via an App Group container.
// Host app — present the document picker targeting your provider domain
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
@State private var showPicker = false
@State private var pickedURL: URL?
var body: some View {
Button("Open from My Provider") { showPicker = true }
.sheet(isPresented: $showPicker) {
DocumentPicker(contentTypes: [.item]) { url in
pickedURL = url
}
}
}
}
struct DocumentPicker: UIViewControllerRepresentable {
let contentTypes: [UTType]
var onPick: (URL) -> Void
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let vc = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes)
vc.delegate = context.coordinator
vc.allowsMultipleSelection = false
return vc
}
func updateUIViewController(_: UIDocumentPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(onPick: onPick) }
final class Coordinator: NSObject, UIDocumentPickerViewControllerDelegate {
let onPick: (URL) -> Void
init(onPick: @escaping (URL) -> Void) { self.onPick = onPick }
func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
// Start security-scoped access before reading
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
onPick(url)
}
}
}
Full implementation
The File Provider Extension target is a separate bundle inside your app. Its principal class must conform to NSFileProviderReplicatedExtension — the modern, on-demand syncing API that replaced the legacy NSFileProviderExtension. You also need an NSFileProviderItem model and an NSFileProviderEnumerator to list your container's contents. The host-app side registers a domain at launch and presents the picker.
// ─── File Provider Extension target ─────────────────────────────────────────
import FileProvider
import UniformTypeIdentifiers
// MARK: - Item model
struct FileProviderItem: NSFileProviderItem {
let itemIdentifier: NSFileProviderItemIdentifier
let parentItemIdentifier: NSFileProviderItemIdentifier
let filename: String
let contentType: UTType
let documentSize: NSNumber?
let childItemCount: NSNumber?
let capabilities: NSFileProviderItemCapabilities
// Convenience initialiser for a plain file
init(identifier: NSFileProviderItemIdentifier,
parent: NSFileProviderItemIdentifier = .rootContainer,
filename: String,
contentType: UTType = .data,
size: Int? = nil) {
self.itemIdentifier = identifier
self.parentItemIdentifier = parent
self.filename = filename
self.contentType = contentType
self.documentSize = size.map { NSNumber(value: $0) }
self.childItemCount = nil
self.capabilities = [.allowsReading]
}
}
// MARK: - Enumerator (lists items in a container)
final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let containerIdentifier: NSFileProviderItemIdentifier
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier) {
self.containerIdentifier = enumeratedItemIdentifier
super.init()
}
func invalidate() {}
func enumerateItems(for observer: NSFileProviderEnumerationObserver,
startingAt page: NSFileProviderPage) {
// Replace with your real data source
let items: [NSFileProviderItem] = containerIdentifier == .rootContainer
? [FileProviderItem(identifier: .init("doc-1"), filename: "Report.pdf",
contentType: .pdf, size: 204_800)]
: []
observer.didEnumerate(items)
observer.finishEnumerating(upTo: nil)
}
func enumerateChanges(for observer: NSFileProviderChangeObserver,
from anchor: NSFileProviderSyncAnchor) {
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
}
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
completionHandler(NSFileProviderSyncAnchor(Data("v1".utf8)))
}
}
// MARK: - Extension principal class
final class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
required init(domain: NSFileProviderDomain) { super.init() }
func invalidate() {}
// Return metadata for a single item
func item(for identifier: NSFileProviderItemIdentifier,
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress {
if identifier == .rootContainer {
completionHandler(FileProviderItem(identifier: .rootContainer,
filename: "My Provider",
contentType: .folder), nil)
} else {
completionHandler(nil, NSFileProviderError(.noSuchItem))
}
return Progress()
}
// Provide file bytes for download
func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier,
version: NSFileProviderItemVersion?,
request: NSFileProviderRequest,
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress {
// Write data to a temp file, then call completionHandler with its URL
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent(itemIdentifier.rawValue)
try? Data("Hello, Document Provider!".utf8).write(to: tmp)
let meta = FileProviderItem(identifier: itemIdentifier, filename: "Report.pdf",
contentType: .pdf, size: tmp.fileSize)
completionHandler(tmp, meta, nil)
return Progress()
}
// Required mutating stubs (implement for read-write providers)
func createItem(basedOn template: NSFileProviderItem, fields: NSFileProviderItemFields,
contents url: URL?, options: NSFileProviderCreateItemOptions,
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
completionHandler(template, [], false, nil); return Progress()
}
func modifyItem(_ item: NSFileProviderItem, baseVersion: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields, contents: URL?,
options: NSFileProviderModifyItemOptions, request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
completionHandler(item, [], false, nil); return Progress()
}
func deleteItem(identifier: NSFileProviderItemIdentifier,
baseVersion: NSFileProviderItemVersion,
options: NSFileProviderDeleteItemOptions, request: NSFileProviderRequest,
completionHandler: @escaping (Error?) -> Void) -> Progress {
completionHandler(nil); return Progress()
}
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier,
request: NSFileProviderRequest) throws -> NSFileProviderEnumerator {
FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
}
// ─── Host app ────────────────────────────────────────────────────────────────
import SwiftUI
import FileProvider
import UniformTypeIdentifiers
@main
struct ProviderDemoApp: App {
init() { registerDomain() }
var body: some Scene {
WindowGroup { ContentView() }
}
private func registerDomain() {
let domain = NSFileProviderDomain(identifier: .init("com.example.myprovider"),
displayName: "My Provider")
NSFileProviderManager.add(domain) { error in
if let error { print("Domain registration failed:", error) }
}
}
}
struct ContentView: View {
@State private var showPicker = false
@State private var openedFileName: String?
var body: some View {
VStack(spacing: 20) {
if let name = openedFileName {
Label(name, systemImage: "doc.fill")
.font(.headline)
}
Button("Browse My Provider") { showPicker = true }
.buttonStyle(.borderedProminent)
}
.padding()
.sheet(isPresented: $showPicker) {
DocumentPicker(contentTypes: [.item]) { url in
openedFileName = url.lastPathComponent
}
}
}
}
// DocumentPicker struct — see TL;DR above
extension URL {
var fileSize: Int? {
(try? resourceValues(forKeys: [.fileSizeKey]))?.fileSize
}
}
#Preview {
ContentView()
}
How it works
-
Domain registration (
NSFileProviderManager.add(_:)). Calling this at app launch tells the system that your extension is available. The domain'sidentifiermust match the bundle suffix of your extension target. Without this call your provider never appears in Files.app or any document picker. -
NSFileProviderReplicatedExtensionlifecycle. The system launches your extension process on demand and callsitem(for:)to resolve metadata,fetchContents(for:)to stream bytes to a local cache, andenumerator(for:)to list containers. The extension is suspended when idle — it must never hold persistent in-memory state. -
FileProviderEnumeratordrives the file list. TheenumerateItems(for:startingAt:)method is where you return your actual item array (from a database, network, or App Group container). TheenumerateChanges+currentSyncAnchorpair powers incremental updates so the system only re-downloads changed items. -
Security-scoped URLs in the picker callback. URLs handed back by
documentPicker(_:didPickDocumentsAt:)are security-scoped — you must bracket any file access withurl.startAccessingSecurityScopedResource()/stopAccessingSecurityScopedResource()or your reads will fail silently with a sandbox error. -
App Group shared container. Your host app and the extension are separate processes. Use
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)pointing at a shared App Group to exchange files without XPC boilerplate. Add the same App Group identifier to both targets' entitlements files.
Variants
Read-write provider with conflict resolution
// In modifyItem — detect conflicts via baseVersion
func modifyItem(_ item: NSFileProviderItem,
baseVersion: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields,
contents newContents: URL?,
options: NSFileProviderModifyItemOptions,
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
guard let newContents else {
completionHandler(item, [], false, nil)
return Progress()
}
// Compare baseVersion against server version to detect conflicts
let serverVersion = fetchServerVersion(for: item.itemIdentifier)
if baseVersion.contentVersion != serverVersion {
// Signal conflict — system will call createItem with a conflict copy
completionHandler(nil, [], false,
NSFileProviderError(.newerExtensionVersionRequired))
return Progress()
}
// Safe to upload new contents
upload(contents: newContents, for: item.itemIdentifier) { updated, error in
completionHandler(updated, [], false, error)
}
return Progress()
}
// Helper stubs
func fetchServerVersion(for id: NSFileProviderItemIdentifier) -> Data { Data() }
func upload(contents url: URL, for id: NSFileProviderItemIdentifier,
completion: @escaping (NSFileProviderItem?, Error?) -> Void) {
completion(nil, nil)
}
Export (move) instead of open
To let users export a file into your provider's storage rather than open one from it, initialise the picker with UIDocumentPickerViewController(forExporting:asCopy:) and set a destination URL inside your App Group container. The extension's createItem(basedOn:) will be called after the copy completes, giving you a chance to sync the new file to your back-end.
Common pitfalls
-
Missing entitlements crash at runtime. Both the host app target and the extension target need the File Provider entitlement (
com.apple.developer.fileprovider.testing-modeduring development) and a matching App Group. Forgetting either causes silent domain-registration failures — check the Console forNSFileProviderErrorDomainerrors. -
NSFileProviderExtension(legacy) vsNSFileProviderReplicatedExtension. The legacy class was deprecated in iOS 16 and removed from Simulator in iOS 17 beta builds. If your Xcode template still generates a subclass ofNSFileProviderExtension, delete it and adopt the replicated protocol — the method signatures are completely different. - Never store state in the extension process. The system can terminate your extension at any time between calls. Persist everything (item metadata, sync anchors, cached file paths) to the shared App Group container database (e.g., SwiftData or SQLite). Reading from in-memory caches across process launches will cause inconsistent enumerations.
-
Security-scoped resource leaks. Every call to
startAccessingSecurityScopedResource()must be balanced with astopAccessingSecurityScopedResource(). Use adeferblock immediately after the guard or your app will accumulate resource leaks that silently fail after hitting the system limit (~256 open scoped URLs per process).
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement a document provider in SwiftUI for iOS 17+. Use NSFileProviderReplicatedExtension, NSFileProviderItem, NSFileProviderEnumerator, and UIDocumentPickerViewController. Add a File Provider Extension target with correct entitlements. Share data between app and extension via an App Group container. Make the host-app picker accessible (VoiceOver labels on buttons). Add a #Preview with realistic sample data showing the picker trigger.
In the Soarias Build phase, paste this prompt directly into a new feature branch — Soarias will scaffold the extension target, wire the entitlements, and generate the boilerplate enumerator so you can focus on your actual data source.
Related
FAQ
Does this work on iOS 16?
NSFileProviderReplicatedExtension was introduced in iOS 16.0, so the extension class itself is available there. However, Apple deprecated the legacy NSFileProviderExtension in iOS 16 and strongly recommends the replicated protocol even for apps targeting iOS 16. For iOS 17+ you get additional APIs like NSFileProviderRequest.isSystemRequest and improved conflict-detection hooks, so targeting iOS 17 minimum is the pragmatic choice for new providers.
How do I show a custom UI inside the document picker instead of the system Files chrome?
Add a second extension target of type Document Picker UI Extension. Its principal class subclasses UIDocumentPickerExtensionViewController and you can embed any SwiftUI view via UIHostingController inside viewDidLoad. The custom UI is shown when the user taps your provider in the location sidebar. This is separate from the File Provider extension — you need both targets for full custom-UI support.
What is the UIKit equivalent?
In UIKit you present UIDocumentPickerViewController directly and implement UIDocumentPickerDelegate on your view controller — no UIViewControllerRepresentable wrapper needed. The File Provider extension side (NSFileProviderReplicatedExtension, NSFileProviderItem, etc.) is identical between UIKit and SwiftUI apps since it runs in a completely separate process.
Last reviewed: 2026-05-12 by the Soarias team.