背景:EXIF MasterにおけるHero Transition要件
EXIF Masterの開発過程で、写真リストページと写真詳細ページの間でHero Transitionのような遷移効果を追加したいと考えました。 SwiftUIは最近のいくつかのバージョンで以下を提供しています:
.navigationTransition(.zoom).matchedTransitionSource
API設計から判断すると、これが公式の標準的な解決策です。
目標とする動作は明確です:
- リストページに写真のサムネイルを表示する。
- 写真をタップすると、ズームアニメーションで詳細ページにプッシュ遷移する。
- システムのサイドスワイプジェスチャーを使用して戻る。
- 戻った後、元のリストの写真は表示されたままであること。
しかし、実際の動作は期待通りではありませんでした。
現象:ジェスチャーで戻ると元の写真が消える
実機およびシミュレーターで再現された現象は以下の通りです:
- 写真詳細ページへのプッシュ時のアニメーションは正常。
- 戻るボタンで戻る場合、ほとんどの場合は正常。
- サイドスワイプジェスチャー(インタラクティブポップ)で戻った後:
- 元のリストにあったその写真が直接消失する。
- データ状態の喪失ではなく、ビューが不可視になる。
navigationTransition / matchedTransitionSourceに関連するすべてのコードを削除すると、問題は即座に解消する。
これは次のことを意味します:
問題はデータ、状態、Diffable更新とは無関係であり、アニメーションメカニズムに強く関連している。
調査プロセス:なぜこれが「自分のミス」ではないと言えるのか
これがフレームワークの問題であることを確認する前に、以下の調査を行いました:
- データソースが変更されていないことを確認(配列、ID、Bindingは安定)。
.id()を使用してビューの強制更新を試行。- ImageをColor / Rectangleに置き換え。
- 遅延ロード(LazyVGrid / LazyVStack)を削除。
- アニメーション、トランザクション、
withAnimationを無効化。
結論は非常に一貫していました:
navigationTransitionを削除すると、問題は消失する
このような挙動は、「フレームワークレベルのレンダリングまたはライフサイクルのバグ」の特徴と強く一致しています。
最終確認:これはSwiftUIの公式バグである
完全な再現条件をGPTに説明した後、得られた結論は以下の通りです:
- これはSwiftUI NavigationStack + navigationTransitionの既知の問題である。
- 個別のケースではない。
- 開発者コミュニティで繰り返し報告されている。
- 現在まで、リリースノートに明確な修正の記載は見られない。
影響を受けるシステムバージョンの範囲
Reddit、Stack Overflow、Apple Developer Forumからのフィードバックをまとめると、以下の結論が得られます:
| iOS バージョン | 影響 | 説明 |
|---|---|---|
| iOS 18.x | ❌ なし | 正常に動作 |
| iOS 26.0 (Beta / Release) | ✅ あり | 最初の大規模な報告 |
| iOS 26.1 Beta / RC | ✅ あり | 未修正 |
| iOS 26.1 正式版 | ✅ あり | コミュニティからの継続的な報告 |
| iOS 26.2 以降 | ⚠️ 高確率であり | 明確な修正確認はまだない |
主要な特徴:
- iOS 26シリーズでのみ発生
- インタラクティブポップ(スワイプバック)と強く関連
- matchedTransitionSourceの復元失敗に関連
技術的な原因(推測)
コミュニティの分析を組み合わせると、以下のように合理的に推測できます:
- iOS 26で
NavigationStackの内部実装に変更があった。 matchedTransitionSourceはアニメーション終了後にソースビューの状態を「埋め戻す」必要がある。- ジェスチャーによってアニメーションが中断または早期終了した場合:
- ソースビューがレンダリングツリーに正しく再登録されない。
- 結果として、ビューは存在するが描画には参加しない。
これはAPIの誤用ではなく、ライフサイクル復元パスの欠落です。
実行可能な回避策(不完全)
案1:プッシュの代わりに fullScreenCover を使用する
詳細ページをNavigationStackから移動させます:
@State private var showDetail = false
@Namespace private var ns
var body: some View {
Image(...)
.matchedTransitionSource(id: photo.id, in: ns)
.onTapGesture {
showDetail = true
}
.fullScreenCover(isPresented: $showDetail) {
DetailView(photo: photo)
.navigationTransition(
.zoom(sourceID: photo.id, in: ns)
)
}
}
メリット:
- NavigationStackのバグを回避できる。
- アニメーションは引き続き使用可能。
デメリット:
- システムレベルのプッシュ/ポップの意味合い(セマンティクス)が失われる。
- 閉じるロジックを自分で処理する必要がある。
- UXがネイティブナビゲーションと完全には一致しない。
なぜこの問題は「厄介」なのか
このバグが開発者の時間を大量に消費する理由は以下の通りです:
- 挙動が「状態管理のエラー」のように見える。
- ログ、ブレークポイント、デバッグツールが問題を直接指し示さない。
- サンプルコードや単純なデモでは再現しないことが多い。
- 実際のインタラクション(ジェスチャーバック)の下でのみ安定して出現する。
これが、私がアニメーションを削除する前に長い時間をかけて調査した理由です。
コーディングとトラブルシューティングにおけるAIの価値
プログラミングやトラブルシューティングにおけるAIの価値は非常に明確になってきました。 以前は、このような問題には以下が必要でした:
- 大量のフォーラムやIssueを閲覧する。
- 異なるシステムバージョンを手動で比較する。
- 消去法で結論に徐々に近づく。
しかし現在、AIは情報が高度に分散している状況でも、「これはあなたの問題ではなく、フレームワークの問題である」と迅速に識別し、バージョンの範囲、原因の推測、実行可能な代替案を提示できます。この効率はかつては想像できないものでした。
コラボレーションツールとしてであれ、境界を明確にした後に実装の一部を直接AIに任せるのであれ、開発プロセスは著しく加速されています。開発者は無意味な繰り返しのデバッグから時間を解放され、真に重要な問題を考えることに時間を割くことができます: 製品の機能は合理的か?インタラクションは人間の実際の使用習慣に合っているか?そして、システムは実際の環境でどのように動作するか?