macOS File Coordination: NSFileCoordinator and NSFilePresenter Without Deadlocks
How coordinated reads, writes, moves, and snapshots interact with file presenters, accessor blocks, operation queues, packages, and cloud-backed documents.
On macOS, two processes can legitimately work with the same document: an editor saves it, a sync provider replaces it, Finder moves it, and a backup tool reads it. NSFileCoordinator and NSFilePresenter let those participants announce operations and respond to changes without treating every pathname as an isolated local file.
Coordination is not a global lock and it does not turn a sequence of filesystem calls into a transaction. It arranges access among cooperating participants. The accessor block, presenter callbacks, queue choice, and URL returned by the coordinator form the correctness boundary.
A presenter represents an interested object
An object conforming to NSFilePresenter declares a primary presented item URL and an operation queue on which Foundation delivers notifications. It can respond when an item moves, changes, is deleted, gains a version conflict, or needs to relinquish access for another participant.
Register a presenter with NSFileCoordinator.addFilePresenter(_:) only after it is fully initialized, and remove it before teardown. Registration retains the presenter. Forgetting to unregister can keep a document controller alive and deliver callbacks after the surrounding user interface appears closed.
The presented item operation queue must serialize state that callbacks mutate. Do not point every presenter at the main queue merely for convenience. A slow coordination callback can block another process’s file operation, while a callback that synchronously waits for main-thread work can deadlock if the main thread is waiting for coordination.
Coordinate one operation at a time
Create a coordinator for a logical operation, optionally associating it with the presenter that initiated the access. The synchronous APIs accept URLs, options, and an accessor block. Foundation calls the accessor only after coordination is granted.
let coordinator = NSFileCoordinator(filePresenter: documentPresenter)
var coordinationError: NSError?
coordinator.coordinate(
writingItemAt: originalURL,
options: .forReplacing,
error: &coordinationError
) { coordinatedURL in
try? replacementData.write(to: coordinatedURL, options: .atomic)
}
Use the URL passed into the accessor. A presenter may have moved the item while the request waited, making the original URL stale. Perform the protected file access inside the block and return promptly. Dispatching the actual write asynchronously from inside the accessor releases coordination before the write occurs.
The synchronous API returning is not proof the file operation succeeded. Check both the coordination error and the error from the read, write, move, or replacement itself. Keep them distinct in logs so an unavailable presenter is not confused with a disk-full failure.
Read and write options describe intent
Coordinated reads can coexist when no writer requires exclusive access. A coordinated writer waits until conflicting readers and writers relinquish the item. Options refine the operation: replacing content, moving an item, deleting it, or reading a version suitable for upload have different effects on presenters.
Select the narrowest accurate intent. Reporting a normal modification as deletion causes unnecessary presenter behavior; writing a directory without the appropriate directory-oriented semantics may leave consumers observing an inconsistent package.
For a move, coordinate the source and destination as one operation through the API designed for both URLs, perform the filesystem move in the accessor, and notify the coordinator of the move when required by the chosen interface. Presenters can then update their own URLs instead of continuing to observe an obsolete path.
Uploading a directory produces a temporary snapshot
The .forUploading reading option asks coordination for a stable upload representation. For a directory, Foundation can provide a temporary archive rather than the original live tree. That lets a sync process upload one consistent snapshot while other work later changes the directory.
The coordinated URL may therefore refer to a temporary item whose lifetime is tied to the accessor. If another asynchronous system needs the file later, open the upload stream or move/copy the snapshot into application-owned temporary storage while still inside the block. Merely retaining the URL string does not extend the coordinated resource’s lifetime.
An upload snapshot does not prove remote durability. The application still needs checksums, retry behavior, conflict policy, and confirmation from the destination. Coordination only stabilizes the local source for the protected read.
Asynchronous intents scale complex access
NSFileAccessIntent describes reading or writing one or more URLs, and the coordinator can arrange those intents before calling a completion queue. This is useful when an operation involves several items or should not block its initiating thread while presenters respond.
The same lifetime rule applies: perform all coordinated access during the granted section, using each intent’s URL. Keep the completion queue independent from presenter queues to avoid circular waits. If the operation needs user input, gather it before requesting access rather than holding coordination while a dialog remains open.
Avoid coordinating from a presenter’s own callback unless the documentation for that callback and operation requires it. Presenter methods already run as part of a coordination exchange. Starting an overlapping synchronous request from there is a common route to deadlock.
Packages need whole-object thinking
A document package appears as one document to the user but is a directory on disk. Saving children independently can expose a half-old, half-new package to a backup or sync reader. Coordinate at the package URL when consistency applies to the package as a unit, then build the replacement in temporary storage and exchange it in the accessor.
For large documents, keep expensive computation outside coordination. Prepare serialization, compression, and validation first. Acquire coordinated write access only for the shortest part that reads the last live state, resolves conflicts, and commits the replacement.
File presenters must handle being asked to relinquish and later reacquire access. Flush in-memory edits before yielding when necessary, release file descriptors or caches that prevent a move, and rebuild state from the new coordinated URL afterward. A callback that simply acknowledges relinquishment without making the object movable defeats the protocol.
Test with genuinely competing participants
A single-process unit test cannot prove coordination. Run two helper processes that register presenters for the same document. Pause one callback, request a write from the other, move the item through Finder or a test coordinator, introduce a version conflict, and terminate a presenter during a pending operation.
Verify no accessor escapes its block, no queue waits synchronously on a queue that depends on it, and every registration has a balanced removal. Add signposts or structured timings around request, grant, file operation, and release so a hang identifies which participant retained coordination.
Test local APFS, a document package, and the cloud or network-backed location the application supports. Coordination behavior is the common contract, but provider latency and replacement semantics reveal different timing bugs.
Correct file coordination is less about wrapping every Data.write call and more about modeling ownership. Presenters declare who cares, coordinators state exactly what is about to happen, accessor blocks bound protected I/O, and operation queues keep callbacks independent. When those roles are explicit, moves and sync updates become events the application can reconcile instead of races it notices after data loss.
Related:
- File Provider on macOS: Domains, Placeholders, and System-Managed Sync
- Fixing ‘Startup Disk Full’ on macOS When Files Don’t Add Up
Sources: