```html SwiftUI: Document Provider Extension (iOS 17+, 2026)

How to implement a document provider in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: Document Provider, NSFileProviderReplicatedExtension Updated: May 12, 2026
TL;DR

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

  1. Domain registration (NSFileProviderManager.add(_:)). Calling this at app launch tells the system that your extension is available. The domain's identifier must match the bundle suffix of your extension target. Without this call your provider never appears in Files.app or any document picker.
  2. NSFileProviderReplicatedExtension lifecycle. The system launches your extension process on demand and calls item(for:) to resolve metadata, fetchContents(for:) to stream bytes to a local cache, and enumerator(for:) to list containers. The extension is suspended when idle — it must never hold persistent in-memory state.
  3. FileProviderEnumerator drives the file list. The enumerateItems(for:startingAt:) method is where you return your actual item array (from a database, network, or App Group container). The enumerateChanges + currentSyncAnchor pair powers incremental updates so the system only re-downloads changed items.
  4. Security-scoped URLs in the picker callback. URLs handed back by documentPicker(_:didPickDocumentsAt:) are security-scoped — you must bracket any file access with url.startAccessingSecurityScopedResource() / stopAccessingSecurityScopedResource() or your reads will fail silently with a sandbox error.
  5. 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

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.

```