Codex のフルレビューを Claude が検証する中で出た、写真選択の入口で起きる「無言の行き止まり」を、勉強のためにまとめたものです。
綴Photo のモバイル版は、カメラロールから写真を選んで、そこに綴り(気持ち)を添えて保存します。その入口の写真ピッカーで、サムネは見えていて選べるのに、タップしても保存シートが開かない——という無言の行き止まりが、iCloud を使っている人に起き得る状態だった。
エラーも出ません、画面も変わりません、ただ反応しません。原因は、タップ処理が「写真の実体ファイルを取れること」を、シートを開く前提条件にしていたこと。しかもその実体ファイルは、後続のどこでも使っていなかった——取得して、そのまま捨てていた。
何が起きていたか
タップしたときの処理は、こうなっていた。
Future<void> onPhotoTapped(AssetEntity asset) async {
final file = await asset.file; // ← 実体ファイルを取りに行く
if (file == null) return; // ← 取れなければ黙って引き返す
// 実際に使うのは asset.id など。file はここから先で一切登場しない
final photo = NewPhotoData(assetId: asset.id, takenAt: asset.createDateTime /* ... */);
await showSaveSheet(photo); // 保存シートを開く
}asset.file は写真の実体ファイル(フル解像度の本体)を取りに行く処理です(await は「取り終わるまで待つ」の意味。iCloud だとこの待ちがクラウドからのダウンロード待ちになります)。これが取れなかったら(null)静かに return します。ところが、その後に作る保存用のデータは asset.id などしか使っておらず、file はどこにも登場しません。つまり「実体ファイルが取れること」を関門にしているのに、その実体ファイル自体は使っていません。取得して捨てています。
最適化がオンの写真をタップすると、裏で本体のダウンロードが走り、その間タップは無反応になります。取れなければユーザーには理由も何も示されません。「写真を選んで綴る」という主導線の入口が、無言で途切れていた。
なぜこう書かれていたのか。「取得して → null チェックして → ダメなら引き返す」という形は防御的なコードに見えますし、asset.file という名前も通信には見えません(ファイルと言われれば、ディスク上のものを読むだけの軽い操作を連想します)。iCloud で本体がクラウドに退避され得ると知らなければ、ここがダウンロードの入口だとは読めません。ただしこの関門は、実際には何も守っていません。守るなら、この後 file を使う処理があるはずです。
重さのズレ
引っかかっていたのは、「表示に要る重さ」と「処理に要る重さ」を取り違えていたことだ。
開発側が無意識に「写真をタップ=実体がある」と前提すると、そのズレが主導線の入口で表に出ます。
直した方向
その関門(await asset.file と null チェック)を削除して、タップしたら asset.id で即シートを開くようにした。実体が本当に要るのはプレビューやアップロードのときなので、その取得はそこへ寄せた——必要になった場所で取りに行けばいい。
Future<void> onPhotoTapped(AssetEntity asset) async {
// 実体取得を入口の前提にしない。assetId で即シートを開く(実体はプレビュー/アップロード時に取る)
final photo = NewPhotoData(assetId: asset.id, takenAt: asset.createDateTime /* ... */);
await showSaveSheet(photo);
}消したのは実質2行。でも消す前に「この file、本当にどこも使っていないか」「実体取得を入口でやる必然があるか」を確かめる必要があった。確かめてみれば、使っていない await が失敗経路だけを足していた、という構図だった。
学びポイント
- 「表示に要る重さ」と「処理に要る重さ」を混同しません。サムネで足りる入口に、フル実体の取得を前提として置くと、重い方が手に入らない環境(iCloud最適化)で無言の行き止まりになります
- iCloud(や類似のオフロード)では「サムネは手元・本体はクラウド」。
asset.fileのような実体取得は隠れた通信になり、失敗し得る前提で扱います - 「取得して捨てている」前提条件は、何も足していないのに失敗経路だけを増やします。使っていない
awaitは、ただのコストではなく行き止まりの種になります - 無言で
returnする前に、ユーザーに何を見せるか——あるいは、そもそもそれを前提にしないか——を考えます
おわりに
このバグは、コードを眺めるだけだと「実体ファイルを取って null チェックしてる、堅いな」で通り過ぎてしまいます。怪しさが見えるのは、「この file は後でどう使われる?」と追ってどこにも使われていないと分かったときと、「iCloud だとこの asset.file は何をする?」と実行環境を当てはめたときだった。
サムネが見えて選べることと、本体が手元にあることがズレます。そのズレが入口の前提条件と重なると、一番大事な導線が静かに塞がります。「見えているから取れる」は、クラウドが間に挟まると成り立たないことがあります。
この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。