When a team debugs an iOS app from a cloud Mac, it is easy to mistake a successful PDF preview from Files for a completed import. The app may only have temporary access to an external file. On the next launch, the original path may no longer work, leaving the reading entry unable to open. The test is not whether the picker appears, but what happens to access, ownership, and file lifetime after the app receives a file from an external provider.
This article uses importing a PDF for long-term reading as its example. The development Mac may be accessed remotely, but the iOS device running the app still determines the rules for file access. Keep the import implementation and automated tests distinct: the former handles URLs delivered by the real picker; the latter checks the app-owned copy and failure paths.
Define what counts as a completed import
Opening a file is not the same as owning it. With UIDocumentPickerViewController(forOpeningContentTypes: [.pdf], asCopy: false), the URL returned in the callback points to a location provided by the picker. Do not save it directly in a reading entry as a permanent path. If the app needs to open the document again later, read the original while access is valid, copy it into the app’s own persistent directory, and point the entry to that copy.
Start with three observable outcomes: canceling the selection creates no entry; a failed copy creates no “successful” entry; and after a successful import, the app can still open its own copy after a relaunch, even if the original has been moved. A temporary directory is suitable for staging one import, not for a long-term library. If an entry must persist, move the validated temporary copy into a persistent app directory such as Application Support.
Treat the import as successful only after the copy has been written and validated, not the moment the picker returns a URL.
Coordinate the copy while access is valid
After receiving the URL in the picker callback, start security-scoped access, then coordinate the read with NSFileCoordinator. The function below demonstrates only how to stage one small PDF. The caller must still decide how to validate its contents, move it into persistent storage, and update the entry. This example uses the asCopy: false open mode; it is not a general-purpose copy function that assumes every source is an externally authorized file.
func stagePickedPDF(_ url: URL) throws -> URL {
guard url.startAccessingSecurityScopedResource() else {
throw CocoaError(.fileReadNoPermission)
}
defer { url.stopAccessingSecurityScopedResource() }
let destination = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("pdf")
var coordinationError: NSError?
var copyError: Error?
NSFileCoordinator().coordinate(
readingItemAt: url,
options: [],
error: &coordinationError
) { readableURL in
do {
try FileManager.default.copyItem(
at: readableURL,
to: destination
)
} catch {
copyError = error
}
}
if let coordinationError { throw coordinationError }
if let copyError { throw copyError }
return destination
}
coordinate is synchronous, so do not let a large copy occupy the UI thread for long. In a production implementation, the entire “start access—coordinate read—stop access” sequence can run in a background task. Do not stop access in the callback and then pass the external URL to that task. Access must also be stopped on error; defer makes that easier to verify than scattered manual cleanup.
Turn the staged file into a recoverable entry
After the copy returns, check that the file exists and has a plausible size, then try opening it with a PDF parser. A .pdf extension alone does not establish that the contents are valid. Once validation succeeds, move the file into a persistent app directory and save its relative path under an app-generated identifier. If validation or the move fails, delete the staged file, show the user an error they can retry, and do not add an item to the reading list.
Persistence also has to account for a partial failure in which the file was written but the entry was not. One approach is to write a pending entry first, then mark it available after the move completes. On launch, scan unfinished entries, check their corresponding files, and clean up anything left behind. The transaction details depend on the app’s data store, but the goal is the same: the list must not contain entries pointing to missing files.
Do not append the display name supplied by the picker directly to the destination path. Generate a filename in the app and store the original name separately for display. This avoids overwriting files with the same name and reduces problems caused by path characters. If the product must always read the external original instead of retaining a copy, that is a different access contract. Design reauthorization and the UI for an unavailable source separately; do not reuse the “imported locally” success state.
Test in layers from the cloud Mac
First, use XCTest to feed PDF fixtures inside the app sandbox into the post-import logic. Check that valid files parse, damaged files produce errors, files with the same name do not overwrite one another, and the list still points to the persistent copy after the staged file is deleted. Do not claim this layer tests security-scoped authorization: sandbox fixtures generally do not have the access conditions of a real file provider.
Next, exercise the full UI in the simulator. Prepare a PDF visible to the Files picker, then test both selecting it and canceling. After importing, quit and relaunch the app to confirm the reading entry opens. Then try moving the source file shown in the picker to see whether the app is actually reading its own copy. Record provider behavior that automation cannot control reliably as a manual acceptance check; do not hide the uncertainty behind a fixed-duration wait.
Finally, recheck authorization and download behavior on the target device with the file providers people will actually use. Some documents have not finished downloading locally when selected. If reading fails, the UI should show the error and allow another selection, not create an empty copy. This check is about error handling, not a promise about how long a provider will take to download a file.
Finish with a failure matrix
| Action | Expected result |
|---|---|
| Cancel selection | Reading list unchanged; no staged file left behind |
| Source cannot be read | Clear error; no successful entry created |
| Copy fails validation | Staged file removed; retry option remains available |
| Move source after import | App-owned copy still opens after relaunch |
| Interrupt app during import | Unfinished entry detected and handled on next launch |
An acceptance record should include the file type, source category, OS and app build versions, and the actual result of each action. When reproducing an issue over remote desktop, the team can then identify whether the failure occurred during authorization, coordinated reading, copying, validation, or entry commit instead of repeatedly selecting the same file.
Frequently asked questions
Can an app save a document picker's URL and reopen it later?
Do not treat an external path as a durable app-owned file. For ongoing use, coordinate the read while access is valid, copy the data into persistent app storage, and reopen that copy.
Are simulator tests enough to validate document imports?
No. They can cover copying, parsing, and errors, but you should also test the actual picker and a file provider for permissions, cancellation, and moved source files.
Need a dedicated physical Mac you can access remotely?
Compare the specs, locations, and billing cycles for M4 Start, M4 Work, and M4 Pro, then choose the plan that fits your workload.