ユーザーが写真アプリから iOS アプリへ画像を共有すると、共有拡張はメニューに表示されるのに、開くと「読み取れません」と表示されることがあります。この問題は、拡張が正常にビルドされたかを確認するだけでは切り分けられません。共有元アプリが渡すのは NSItemProvider で、複数の型を宣言している場合があります。また、ファイル形式の入力では、読み込みコールバックの実行中にしか一時的なパスを利用できないこともあります。「表示される」「読み取れる」「アプリ本体へ渡せる」を分けて検証すると、問題の箇所を特定しやすくなります。
まず入力の契約を定義する
実際にサポートする共有元とデータを選び、「すべての画像」を曖昧な一つのテスト項目にしないでください。少なくとも、共有元アプリ、共有する内容、registeredTypeIdentifiers、想定する読み取り方法、アプリ本体が最終的に受け取るべき結果を記録します。同じ写真でも共有元によってファイル、データ、またはその両方が提供される可能性があります。拡張は配列の先頭にある型だけに依存してはいけません。
| 入力 | 拡張で確認すること | 合格基準 |
|---|---|---|
| 画像 1 枚 | 画像型に適合する表現があるか | 内容を読み取り、受け渡しの記録を作成できる |
| 複数の画像 | 添付項目ごとに宣言された型 | 全件数が成功件数と失敗件数の合計に一致する |
| プレーンテキスト | テキスト表現が提供されるか | 製品要件に従って受け入れるか、明確に拒否する |
| 非対応の内容 | 一致する型がない | 分かりやすいエラーを表示し、空の記録を作成しない |
共有拡張のアクティベーションルールはメニューに表示される条件を決め、実行時の型チェックは現在の添付項目を処理できるかを決めます。両方をテストする必要があります。「メニューに表示された」だけで読み取りの検証を済ませてはいけません。
宣言された型を確認してから表現を選ぶ
開発中は添付項目の型識別子をローカルの診断記録に残します。ただし、ユーザーのコンテンツまで一緒にアップロードしないでください。型を選ぶ際は対象の型に適合するかを確認し、特定の共有元アプリが返す識別子全体をハードコードしないでください。製品が画像だけを受け付けるなら、テキスト入力は明確な拒否処理に進めます。不明な内容を無理に画像としてデコードしないでください。
コールバック内でファイルをコピーする
ファイル表現を扱う際、最も危険なのは loadFileRepresentation が返した URL を保存し、拡張の終了後にアプリ本体で開こうとする実装です。この URL は読み込み処理で提供される一時ファイルを指しており、永続的な受け渡し先としては使えません。
合格基準は「コールバック内でパスを出力できること」ではなく、「コールバック終了後も、アプリ本体がアクセス権を持つコピーを読み取れること」です。
次の Swift コードは、画像ファイルを扱う際の重要な境界を示します。拡張とアプリ本体でファイルを共有する必要がある場合は、同じ 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))
}
}
}
このコードが扱うのは、画像の添付項目 1 件だけです。実際の実装では、複数の添付項目の結果を集約し、受け入れるファイルサイズを制限するとともに、一部だけ成功した場合にコピー済みのファイルを残すか、処理全体を取り消すかを決める必要があります。アプリ本体には管理対象ディレクトリ内のファイル識別子を渡してください。アプリ本体で読み取った後は業務上のルールに従って削除し、コピーを無制限に蓄積しないようにします。
読み取りの境界を自動テストに組み込む
まず型選択関数とコピー関数のテストを書き、その後で共有画面全体をテストします。少なくとも画像型、非対応の型、読み込みエラー、共有コンテナを利用できない場合、複数の添付項目の一部が失敗する場合、同じ名前の入力同士が上書きしないことを確認してください。テストダブルではコールバックのタイミングを制御できるようにします。コールバック内でファイルを提供し、コールバックから戻った後に元ファイルを移動しても、コピー先を読み取れることをアサートします。これで、URL が空でないことだけでなく、ファイルのライフサイクルを検証できます。
CDNMac のクラウド Mac では、まずプロジェクトで公開されている scheme と利用可能なシミュレータを確認し、該当するテストを実行します。次のコマンドを実行するには、3 つの環境変数に現在のプロジェクトの実際の値を設定してください。結果バンドルは新しく作成したディレクトリに保存し、再実行時に既存のパスと衝突しないようにします。
: "${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 に含まれていなかった場合も検知する必要があります。初めて CI に組み込む際は、アサーションを 1 つ意図的に失敗させ、パイプラインが確実に停止することを確認します。
実際の共有でテストの見落としを補う
ユニットテストで自分たちの読み取りロジックは検証できますが、共有元アプリがどの表現を提供するかまでは保証できません。テスト端末を 1 台用意し、サポート予定の共有元から項目ごとに共有します。まず拡張が表示されることを確認し、実際に宣言された型、読み取り結果、アプリ本体に保存されたコピーを調べます。大きな画像や複数画像の入力では、特にメモリ使用量、待機中のフィードバック、キャンセル時の動作を確認してください。小さな画像 1 枚が通っただけで検証を終えてはいけません。
問題を切り分ける際は順番に確認します。拡張が表示されなければアクティベーションルールを確認します。表示されても型が一致しなければ、共有元が提供した識別子を記録します。読み込みに失敗したらエラーの種類を残します。拡張では読み取れたのにアプリ本体で失敗する場合は、App Group の権限、コールバック内でコピーが完了しているか、アプリ本体が受け取ったのがコピー先のパスかを確認してください。テスト記録には必要な型、処理段階、エラー情報だけを残し、共有内容がビルドログに入らないようにします。
リリース前のチェック項目
リリース前に 4 点を再確認します。拡張が宣言する型と実際にサポートする型が一致していること、各ファイルのコピーが読み込みコールバック内で完了すること、アプリ本体がアクセスを許可されたディレクトリ内のコピーだけを読み取ること、拒否・キャンセル・一部失敗のいずれにもユーザーが確認できる結果があることです。App Group の設定を変更した場合は、新規インストールしたテスト端末で受け渡しの一連の処理を再実行してください。開発端末に既存のコンテナがある状態だけで検証してはいけません。
型のマトリクス、自動テストの結果、端末での共有記録を同じ変更にまとめてください。後から入力形式を追加するときの出発点が明確になります。先に入力の契約と失敗するテストを追加し、それから読み取り処理を実装します。ユーザーから「共有したのにファイルがない」と報告されて初めて共有元の動作を推測する事態を避けましょう。
よくある質問
受け取ったファイルの URL をアプリ本体へそのまま渡せますか?
そのまま渡さず、loadFileRepresentation のコールバック内で本体が読める場所へコピーし、コピー先の識別子を渡してください。
シミュレータのテスト後に実機で確認する点は何ですか?
対象アプリから実際に共有し、拡張機能の表示、提供される形式、大きなファイルの処理、本体によるコピー先の読み取りを確認します。
次回のiOSビルドを専有クラウドMacで実行
M4、16GB RAM、256GB SSDを備えた専用物理環境。6つのノードと4種類の利用期間から、作業に合った組み合わせを選べます。