ルーティング先 URL をスタンプしてナビゲーション同一性を一元管理

hotwired/hotwire-native-ios

ルーティング時に UIViewController に URL をスタンプし、同一性判定をコントローラ型ではなくその URL で行うよう変更しました。これにより、同一クラスの異なるインスタンスが URL で正しく区別され、誤った pop/replace が防止されます。

背景

これまで NavigationHierarchyControllervisitingSamePagevisitingPreviousPage は、スタック上のコントローラを Visitable/VisitableViewController にキャストできない場合に 型の同一性 でページを判定していました。カスタムネイティブコントローラが同じクラスでも異なる URL で表示されるケースで、スタック上のインスタンスが区別できず、push すべき場面で pop/replace が走っていました。

この問題は、iOS 側だけでなく Android では既に ルーティング時に位置情報をスタンプ する設計で解決されており、iOS でも同様の単一情報源に統一する必要がありました。

技術的な変更

routedLocation プロパティの追加

UIViewControllerroutedLocation: URL? を associated‑object で付与する拡張が新規作成されました。実装は objc_setAssociatedObject / objc_getAssociatedObject を用い、キーは malloc(1)! で確保したユニークポインタです。

import UIKit

private let routedLocationKey = malloc(1)!

extension UIViewController {
    /// The location the navigator routed this view controller to, stamped at route time.
    var routedLocation: URL? {
        get { objc_getAssociatedObject(self, routedLocationKey) as? URL }
        set { objc_setAssociatedObject(self, routedLocationKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }
}

ルーティング時にスタンプを付与

NavigationHierarchyController.route(controller:proposal:) 内で、ナビゲーション対象のコントローラに controller.routedLocation = proposal.url を設定します。これにより、メイン・モーダルスタックのすべてのルーティング経路で同一情報が付与されます。

else {
    controller.routedLocation = proposal.url
    // …既存ロジックはそのまま
}

同一性判定ロジックの刷新

visitingSamePage(同様に visitingPreviousPage)は、トップコントローラの routedLocation と提案 URL を isSameLocation(as:pathProperties:) で比較します。routedLocation が無い場合はデバッグで logger.warning を出し、リリースビルドでは false を返して新規ページとして push します。

private func visitingSamePage(on navigationController: UINavigationController, via proposal: VisitProposal) -> Bool {
    guard let topVC = navigationController.topViewController else { return false }
    guard let location = topVC.routedLocation else {
        logger.warning("Top view controller \(topVC) has no routed location; treating the visit as a new page.")
        return false
    }
    return location.isSameLocation(as: proposal.url, pathProperties: proposal.properties)
}

テストケースの追加

カスタムコントローラが異なる URL で push、同一 URL で replace、前ページへ戻る pop が正しく行われることを検証するテストが追加され、未スタンプ状態のコントローラがスタックに存在した場合でも安全に push されることを確認しています。

設計判断

objc_setAssociatedObject の採用理由

  • UIKit のスタック変化に追随: バックスワイプやシステムポップといった UI 操作はコントローラ単位で行われ、外部構造に依存しません。コントローラに直接紐付くことで、状態が常に正しいまま保たれます。
  • ライフサイクル管理が自動: オブジェクトが解放されると同時に associated object も解放され、余計なクリーンアップコードが不要です。
  • Android の実装と対等: Android ではバックスタックエントリの引数バンドルに位置情報を保持しており、iOS でも同様の「エントリ単位」保存手段として最も近い実装です。

単一情報源への統一とフォールバック戦略

以前は Visitable.initialVisitableURLroutedLocation を併せて比較していましたが、スタンプが必ず存在する前提に統一し、未スタンプは 警告 + push という安全策に置き換えました。これにより、型比較のバグ が根本的に除去され、開発者は「URL が唯一の同一性指標」だと確信できます。

アサーション vs. ロガー

assertionFailure ではなく logger.warning を使用したのは、開発時にバグを顕在化させつつ、リリースビルドでクラッシュを防ぐためです。UIKit では外部コードが任意に pushViewController できるため、ハードな precondition は実運用上リスクが高くなります。

まとめ

この PR は ルーティング先 URL をコントローラにスタンプし、ナビゲーション同一性の判定を URL のみで行うよう改めました。objc_setAssociatedObject による安全な永続化と、未スタンプ時のフォールバック戦略により、カスタムビューコントローラでも正確な push/replace/pop が保証されます。結果として、iOS のナビゲーションロジックが Android とパリティを取り、同一性バグが根本的に解消されました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
0ce87905

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの5要素が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは `言語:ファイルパス` 形式で正しく記述され、PRリンクも `[#248](URL)` 形式で正しくハイパーリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的なiOS開発者向けの内容で、初心者向けの余計な説明はなく適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論の段落構成になっており、1段落1トピック・トピックセンテンスが先頭に配置され、段落長も適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードはDiffに含まれる変更と実質的に一致しており、抜粋やコメントの省略は機能上の欠落を生んでいません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`associated‑object`、`objc_setAssociatedObject`、`navigation identity` などの用語はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明はPRの概要・変更点と整合しており、因果関係も論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

すべての主張はPRタイトル、説明、Diffに裏付けられており、推測や外部情報の付加はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #248、コード行数等の数値は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRタイトル「Stamp routed location on view controllers for navigation identity」の意味を日本語で適切に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンサポートやリリース日程等、PRに記載のない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PRの記述と一致しています。