團隊在雲端 Mac 上除錯 iOS 應用程式時,常會誤以為:從「檔案」選取 PDF 並成功預覽,就代表匯入已完成。實際上,應用程式可能只是暫時讀到了外部檔案;下次啟動時,原路徑一旦失效,閱讀紀錄就無法開啟。驗收重點不是檔案選取器能否顯示,而是檔案從外部提供者進入應用程式後的存取權限、歸屬與生命週期。
本文以「匯入 PDF 後長期閱讀」為例。開發機可以遠端連線,但檔案存取規則仍由執行應用程式的 iOS 裝置決定。先把匯入實作與自動化測試分開:前者處理選取器實際交付的 URL,後者驗證應用程式擁有的副本與失敗情境。
先界定匯入完成的條件
能開啟檔案,不代表應用程式擁有該檔案。使用 UIDocumentPickerViewController(forOpeningContentTypes: [.pdf], asCopy: false) 時,回呼中的 URL 指向選取器提供的位置,不應直接當作永久路徑存入閱讀紀錄。如果下次啟動仍須開啟檔案,就要趁存取權限有效時讀取原件,複製到應用程式自己的持久目錄,並讓紀錄指向新副本。
先約定三項可觀察的結果:取消選取不新增紀錄;複製失敗不新增「成功」紀錄;匯入成功後,即使原件被移走,重新啟動應用程式仍能開啟自己的副本。暫存目錄適合處理單次匯入,不適合作為長期書庫。需要長期保存紀錄時,再將通過驗證的暫存副本移入 Application Support 等應用程式持久目錄。
應在副本寫入並通過驗證後,才判定匯入成功;選取器傳回 URL 的瞬間還不算完成。
在存取權限有效期間完成協調複製
選取器回呼取得 URL 後,先啟用安全範圍資源存取,再以 NSFileCoordinator 協調讀取。以下函式只示範單一小型 PDF 的暫存步驟;呼叫端仍須決定如何驗證內容、移入持久目錄及更新紀錄。此範例採用 asCopy: false 的開啟模式,不適合直接當成一律將所有來源視為外部授權檔案的通用複製函式。
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 是同步呼叫,不要讓大型檔案的複製作業長時間占用介面執行緒。實際實作可以將「啟用存取—協調讀取—停止存取」整個流程安排在背景工作中;不要先在回呼中停止存取,再把外部 URL 交給背景工作。錯誤路徑也必須停止存取;在這裡使用 defer,比散落各處的手動關閉更容易檢查。
將暫存檔案轉為可恢復的紀錄
複製完成後,先確認副本存在、檔案大小合理,並讓 PDF 解析器嘗試開啟;光有 .pdf 副檔名無法證明內容正確。驗證成功後,再移至應用程式持久目錄,並以應用程式產生的識別碼儲存相對路徑。若驗證或移動失敗,應清除暫存檔案、向使用者顯示可重試的錯誤,不要寫入閱讀清單。
持久化時還要考慮「檔案已寫入,紀錄卻未寫入」的中途失敗。可以先寫入待提交紀錄,完成移動後再標記為可用;啟動時掃描未完成紀錄,核對對應檔案並清理殘留。具體的交易處理方式取決於應用程式如何儲存資料,但目標相同:清單中不能出現指向不存在檔案的項目。
不要將選取器提供的顯示名稱直接串接成目標路徑。使用應用程式產生的檔名,並將原名另存為顯示欄位,既可避免同名檔案相互覆蓋,也能減少路徑字元造成的意外。如果產品要求始終讀取外部原件,而不是保留副本,那就是另一種存取契約:必須另外設計重新授權及來源不可用時的介面,不能沿用「已匯入本機」的成功狀態。
在雲端 Mac 上分層驗收
先用 XCTest 將應用程式沙盒內的 PDF 樣本交給匯入後的邏輯:檢查解析成功、損毀檔案會回報錯誤、同名檔案不會被覆蓋,以及刪除暫存檔案後,清單是否仍指向持久副本。這一層測試不應宣稱已驗證安全範圍資源的授權;沙盒樣本通常不具備真實檔案提供者的存取條件。
接著在模擬器上走完整介面流程:準備一份可從「檔案」選取器看到的 PDF,分別執行選取與取消;匯入後結束並重新啟動應用程式,確認閱讀紀錄仍能開啟。再試著移走選取器中的來源檔案,觀察應用程式是否確實讀取自己的副本。對於自動化無法穩定操控的提供者行為,應列為人工驗收項目,不要用等待固定秒數來掩蓋不確定性。
最後在目標裝置,以及實際會使用的檔案提供者上,重新確認授權與下載行為。有些文件在選取當下尚未完整下載到本機;讀取失敗時,介面應保留錯誤資訊並允許重新選取,而不是建立空白副本。這裡驗收的是錯誤處理,不是替檔案提供者保證下載時間。
以失敗情境矩陣收尾
| 操作 | 預期結果 |
|---|---|
| 取消選取 | 閱讀清單不變,沒有殘留暫存檔案 |
| 無法讀取來源 | 明確顯示錯誤,不建立成功紀錄 |
| 副本驗證失敗 | 清除暫存檔案,保留重試入口 |
| 匯入後移走來源 | 重新啟動後仍能開啟應用程式副本 |
| 匯入時中斷應用程式 | 下次啟動能辨識並處理未完成紀錄 |
驗收紀錄最好附上使用的檔案類型、來源類別、系統與應用程式建置版本,以及每項操作的實際結果。這樣團隊透過遠端桌面重現問題時,就能先判斷失敗發生在授權、協調讀取、複製、驗證還是紀錄提交,而不必反覆點選同一份檔案。
常見問題
可以保存文件選取器傳回的路徑,留待下次開啟嗎?
不要把外部路徑當成永久參照。若需要長期使用,應在存取有效期間協調讀取並複製至 App 的持久目錄,之後從沙盒副本開啟。
只用模擬器就能完成文件匯入驗收嗎?
不能。模擬器適合測試複製、解析與錯誤處理;仍須透過實際文件選取器驗證授權、取消選取及來源檔案移動等情況。
需要一台可遠端連線的獨享實體 Mac?
比較 M4 Start、M4 Work 與 M4 Pro 的規格、節點和計費週期,選擇最符合需求的方案。