履歴位置のリフレッシュでスナップショットを復元せず最新ページを取得
Turbo の refreshVisitable がスナップショットキャッシュを復元していた問題を解消し、.replace アクションで常にサーバーの最新状態を取得できるようにしました。この変更により、非安全なフォーム送信後のリダイレクトでも古いキャッシュが表示されることがなくなります。
背景
refreshVisitable は Turbo のバックナビゲーションと同様に .restore アクションを使用していました。.restore はキャッシュされたスナップショットが存在すればそれを優先的に表示し、履歴スタックには新たなエントリーを追加しませんでした。非安全な(POST/PUT/DELETE/PATCH)フォーム送信後にサーバーが /refresh_historical_location にリダイレクトすると、ユーザーは 「リフレッシュ」 という意図でページの最新状態を期待しますが、キャッシュが残っていると古いコンテンツが再表示される不整合が発生しました。
この不整合は、/page_1 → /page_2 の遷移後にフォーム送信で /page_1 にリダイレクトした際、2回目以降にキャッシュが復元されて新しいサーバー応答が隠れるという形で再現されました(#234 での報告参照)。その結果、ユーザー体験が損なわれるだけでなく、Turbo の仕様(非安全な送信後はキャッシュをクリアして再取得)と iOS 実装の挙動が乖離していました。
技術的な変更
Navigator.swift の refreshVisitable メソッドで .restore → .replace へアクションを置き換えました。.replace はキャッシュを無視して対象ページを再取得し、履歴スタックに追加しない点は .restore と同様です。具体的な差分は以下の通りです。
@@ -253,9 +253,9 @@ extension Navigator: NavigationHierarchyControllerDelegate {
func refreshVisitable(navigationStack: NavigationHierarchyController.NavigationStackType, newTopmostVisitable: any Visitable) {
switch navigationStack {
case .main:
- session.visit(newTopmostVisitable, action: .restore)
+ session.visit(newTopmostVisitable, action: .replace)
case .modal:
- modalSession.visit(newTopmostVisitable, action: .restore)
+ modalSession.visit(newTopmostVisitable, action: .replace)
}
}
}
テストコードも同様に期待アクションを .replace に更新し、変更が正しく反映されていることを検証しています。
@@ -87,7 +87,7 @@ final class NavigationHierarchyControllerHistoricalLocationTests: XCTestCase {
XCTAssertTrue(session.visitWasCalled)
- XCTAssertEqual(session.visitAction, .restore)
+ XCTAssertEqual(session.visitAction, .replace)
}
これにより、手動テストと自動テストの両方で、リフレッシュ後にスナップショットが利用されず新しいページが描画されることが保証されます。
設計判断
アクション置換のみで対応 する方針が採られました。代替案として新しい設定キーやオプションフラグを導入する検討もありましたが、.replace は既存の TurboSession.visit(_:action:) に既に実装されているため、ロジックの追加や後方互換性のリスクを最小化できます。さらに、.replace は履歴スタックへの追加を行わない点で .restore と同等の振る舞いを保ちつつ、キャッシュ無視という期待動作を実現します。
この選択は「最小限の変更で期待される振る舞いを満たす」ことを優先し、コードベース全体への影響を限定的に抑える設計判断といえます。結果として、iOS 側の挙動が Turbo の仕様と一致し、プラットフォーム間の一貫性が向上しました。
まとめ
refreshVisitable が .restore を使っていたために発生していたスナップショット再利用のバグを、.replace に置き換えるだけで解消しました。これにより、非安全なフォーム送信後のリフレッシュが常に最新サーバー状態を取得し、ユーザーが期待する「ページのリロード」動作を提供します。変更はコード規模が小さく、既存テストでカバーされているためリリースリスクも低く、今後の開発で同様のシナリオが安全に扱える基盤が整いました。