使用者從相簿將圖片分享到 iOS App,分享延伸功能出現在選單中,點開後卻提示「無法讀取」。這類問題不能只檢查延伸功能是否成功編譯:來源 App 提供的是 NSItemProvider,它可能宣告多種型別;以檔案形式提供的輸入,也可能只在載入回呼期間有可用的暫存路徑。把「能顯示」「能讀取」「能交給主 App」分開驗收,才能找出問題出在哪個環節。
先列出輸入契約
選定實際需要支援的來源與資料,不要把「所有圖片」寫成一項模糊的測試。至少記錄來源 App、來源內容、registeredTypeIdentifiers、預期讀取方式,以及主 App 最終應取得的結果。同一張圖片從不同來源分享,可能以檔案、資料或兩者的形式提供;延伸功能不能只依賴陣列中的第一個型別。
| 輸入 | 延伸功能應檢查 | 驗收結果 |
|---|---|---|
| 一張圖片 | 是否有符合圖片型別的表示形式 | 能讀取內容並建立交接紀錄 |
| 多張圖片 | 每個附件各自宣告的型別 | 附件數量與成功、失敗數量一致 |
| 純文字 | 是否提供文字表示形式 | 依產品需求接收或明確拒絕 |
| 不支援的內容 | 沒有相符型別 | 顯示容易理解的錯誤,不建立空白紀錄 |
分享延伸功能的啟用規則,決定它何時出現在選單中;執行時的型別檢查,決定它能否處理目前的附件。兩者都要測,不能用「選單裡看得到」取代讀取驗收。
先看宣告,再選表示形式
開發時,將附件的型別識別碼寫入本機診斷紀錄,但不要連同使用者內容上傳。選擇型別時,應檢查它是否符合目標型別,而不是寫死某個來源 App 提供的完整識別碼。若產品只接收圖片,就讓文字輸入走明確的拒絕流程;不要試圖把未知內容強行解碼成圖片。
在回呼中複製檔案
處理檔案表示形式時,最危險的做法是儲存 loadFileRepresentation 回傳的 URL,等延伸功能結束後才讓主 App 開啟。這個 URL 指向載入期間提供的暫存檔案,不能當作長期有效的交接位址。
驗收標準不是「能在回呼中印出路徑」,而是「回呼結束後,主 App 仍能讀到自己有權存取的副本」。
以下 Swift 程式碼示範圖片檔案的關鍵處理邊界。若延伸功能與主 App 需要共用檔案,應設定同一個 App Group,並透過部署設定傳入群組識別碼;不要將範例識別碼直接寫進專案。複製目標使用新檔名,避免覆蓋另一筆分享作業。
import Foundation
import UniformTypeIdentifiers
enum ShareCopyError: Error {
case unsupportedType
case containerUnavailable
case missingFile
}
func copySharedImage(
from provider: NSItemProvider,
groupID: String,
completion: @escaping (Result<URL, Error>) -> Void
) {
guard let typeID = provider.registeredTypeIdentifiers.first(where: {
UTType($0)?.conforms(to: .image) == true
}) else {
completion(.failure(ShareCopyError.unsupportedType))
return
}
provider.loadFileRepresentation(forTypeIdentifier: typeID) { source, error in
if let error {
completion(.failure(error))
return
}
guard let source else {
completion(.failure(ShareCopyError.missingFile))
return
}
guard let container = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupID) else {
completion(.failure(ShareCopyError.containerUnavailable))
return
}
do {
let directory = container.appendingPathComponent(
"Incoming", isDirectory: true
)
try FileManager.default.createDirectory(
at: directory, withIntermediateDirectories: true
)
let ext = UTType(typeID)?.preferredFilenameExtension ?? "bin"
let target = directory.appendingPathComponent(
UUID().uuidString + "." + ext
)
try FileManager.default.copyItem(at: source, to: target)
completion(.success(target))
} catch {
completion(.failure(error))
}
}
}
這段程式碼只處理單一圖片附件。實際實作還需彙整多個附件的結果、限制可接受的檔案大小,並決定部分成功時要保留已複製的檔案,還是全部復原。交給主 App 的應是受控目錄中的檔案識別資訊;主 App 讀取後,再依業務規則清理,不能讓副本無限累積。
將讀取邊界納入自動化測試
先為型別選擇函式與複製函式撰寫測試,再測試完整的分享介面。測試至少要涵蓋圖片型別、不支援的型別、載入錯誤、共用容器無法使用、多個附件部分失敗,以及同名輸入不會互相覆蓋。測試替身應能控制回呼時機:在回呼期間提供檔案,回呼返回後移除來源檔案,再確認複製目標仍可讀取。如此驗證的是檔案生命週期,而不只是一個非空的 URL。
在 CDNMac 雲端 Mac 上,先查看專案公開的 scheme 與可用的模擬器,再執行對應測試。以下命令要求執行者將三個環境變數設為目前專案的真實值;結果套件放進新建目錄,避免重新執行時與既有路徑衝突。
: "${PROJECT:?set PROJECT to an existing .xcodeproj path}"
: "${SCHEME:?set SCHEME to a shared test scheme}"
: "${SIMULATOR_ID:?set SIMULATOR_ID to an available device ID}"
xcodebuild -list -project "$PROJECT"
xcrun simctl list devices available
RESULT_ROOT="$(mktemp -d)"
xcodebuild -project "$PROJECT" \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-resultBundlePath "$RESULT_ROOT/share-input.xcresult" \
test
CI 應保存測試結果套件,並以測試程序的結束狀態判定工作是否成功。不要只搜尋日誌中的「passed」:延伸功能啟動失敗,或目標測試未納入 scheme,也都需要被發現。首次接入時,刻意讓一項斷言失敗,確認流程確實會因此中止。
用實際分享補足測試盲點
單元測試能驗證自己的讀取邏輯,卻無法保證來源 App 會提供哪種表示形式。選一台測試裝置,從預計支援的來源逐項分享:先確認延伸功能出現,再檢查實際宣告的型別、讀取結果,以及主 App 中的副本。大圖片與多張圖片輸入尤其需要觀察記憶體用量、等待時的回饋及取消行為;不要只憑一張小圖通過就結束驗收。
排查問題時依序定位:沒有出現,就檢查啟用規則;出現但型別不符,就記錄來源提供的識別碼;載入失敗,就保留錯誤類別;延伸功能讀取成功但主 App 失敗,就核對 App Group 權限、是否在回呼中完成複製,以及主 App 取得的是否為副本路徑。測試紀錄只保留必要的型別、階段與錯誤資訊,避免將分享內容寫入建置日誌。
交付前的檢查項目
發布前確認四件事:延伸功能的宣告與實際支援的型別一致;每個檔案都在載入回呼中完成複製;主 App 只讀取授權目錄中的副本;拒絕、取消及部分失敗都有可見的結果。若變更了 App Group 設定,還要在全新安裝的測試裝置上重新跑一次交接流程,不能只驗證開發機上既有的容器。
將型別矩陣、自動化測試結果與裝置分享紀錄放在同一次變更中,日後新增輸入格式時,就有明確的起點:先補上契約與失敗測試,再加入讀取實作,而不是等使用者回報「分享後沒有檔案」才回頭猜測來源。
常見問題
延伸功能取得的檔案 URL 能直接交給主 App 嗎?
不要直接交接。應在 loadFileRepresentation 回呼內複製到主 App 可讀取的位置,再傳遞新檔案的識別資訊。
模擬器測試通過後,實機還要確認什麼?
從目標來源 App 實際分享,確認延伸功能是否出現、提供的類型、大型檔案的處理結果,以及主 App 能否讀取複製後的檔案。
讓下一次 iOS 建置在專屬雲端 Mac 上執行
採用 M4、16GB RAM 與 256GB SSD 的固定實體環境,可從六個節點與四種租期中,選擇最適合目前任務的組合。