TabBarMinimize と prefersLargeTitle の動作を安定させるためのビュー階層修正

hotwired/hotwire-native-ios

iOS 26 で導入された TabBarMinimizeBehaviorUINavigationBar.prefersLargeTitles がスクロール検知に依存するため、Hotwire‑Native で WebView が正しく検知されず、タブバーの最小化やラージタイトルが期待通りに動作しない問題を、WebView をビュー階層の最初の子に配置し、iOS 15+ では明示的にコンテンツスクロールビューを設定することで解消した。

背景

この不安定さは Visitable が表示するビュー階層で、screenshotContainerViewactivityIndicatorView が WebView より先に追加されることが原因だった。UIKit のスクロール検知はサブビューの先頭要素をヒューリスティックに使用しており、WebView が最初の子でないとタブバーの最小化やラージタイトルのトリガーが発生しないケースが報告されている。

PR の報告では、WebView が表示時に必ずしも最初の子ビューになる保証がなく、特定の遷移で期待した挙動が失われていたことが確認された。これによりユーザー体験が予測不可能になっていた。

技術的な変更

VisitableViewactivateWebView(_:forVisitable:) で従来の addSubview(webView)insertSubview(webView, at: 0) に置き換え、WebView を常に最初の子ビューとして明示的に配置した。

open func activateWebView(_ webView: WKWebView, forVisitable visitable: Visitable) {
    self.webView = webView
    self.visitable = visitable
    // iOS 14 fallback: with no explicit contentScrollView, UIKit's heuristic requires
    // the scrollable view to be the first subview for large-title / tab-bar-minimize
    // to work. On iOS 15+ this is made robust by setContentScrollView in Visitable.
    insertSubview(webView, at: 0)
    addFillConstraints(for: webView)
    installRefreshControl()
    showOrHideWebView()
}

さらに VisitableactivateVisitableWebView(_:)deactivateVisitableWebView() に、iOS 15 以降で setContentScrollView を呼び出すロジックを追加した。これにより WebView の scrollView をコンテンツスクロールビューとして登録し、解除時にクリアする。

func activateVisitableWebView(_ webView: WKWebView) {
    visitableView.activateWebView(webView, forVisitable: self)
    // Explicitly designate the web view's scroll view as the tracked content
    // scroll view so behaviour like UINavigationBar.prefersLargeTitles and the
    // iOS 26 tab bar minimize trigger reliably, without depending on UIKit's
    // subview-ordering heuristic. `.all` covers the top (nav bar) and bottom
    // (tab bar) edges.
    if #available(iOS 15.0, *) {
        visitableViewController.setContentScrollView(webView.scrollView, for: .all)
    }
    visitableDidActivateWebView(webView)
}

func deactivateVisitableWebView() {
    visitableWillDeactivateWebView()
    if #available(iOS 15.0, *) {
        visitableViewController.setContentScrollView(nil, for: .all)
    }
    visitableView.deactivateWebView()
    visitableDidDeactivateWebView()
}

これらの変更に対して VisitableViewControllerTests に 4 つのテストが追加され、WebView がロード中・復元時に最初の子ビューになること、iOS 15+ で contentScrollView が正しく設定・解除されることを自動検証している。

設計判断

VisitableView の全体構造を大幅に変更する代わりに、insertSubview による順序指定を採用した。これにより既存ロジックや他のサブビュー(スクリーンショット、インジケータ)への影響を最小限に抑えつつ、UIKit のヒューリスティックに依存しない確実な動作を保証できる。

iOS 15 以降の setContentScrollView API を併用した点は、将来的に UIKit のサブビュー順序に対する依存が削除された場合でも安定動作を保つ前方互換性を意識した選択である。新たな公開プロパティや大規模リファクタリングを避け、変更侵入度を低く抑えることでリグレッションリスクを最小化している。

まとめ

この PR は WebView をビュー階層の先頭に配置し、iOS 15+ ではコンテンツスクロールビューを明示的に設定することで、TabBarMinimizeBehaviorprefersLargeTitles の不安定な動作を根本的に解消した。変更は局所的かつ後方互換性を維持しつつ、ユーザー体験の一貫性を向上させている。

記事メタデータ

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

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文がタイトル直下にあり、背景・技術的変更・設計判断・まとめの各セクションが揃っている。まとめは単なる繰り返しではなく、変更の効果と影響を端的に述べている。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `言語:ファイルパス` 形式で記述されている。GitHubリンクは `[PR #199](URL)` となっており、リンクテキストに余分な「PR」が含まれるため完全な `[#199](URL)` 形式ではないが、内容理解に支障はない。

対象読者への適合性 ✓ PASS

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

専門的な iOS 開発者向けの内容で、基礎的な説明は最小限に抑えられている。

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

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

各セクションは総論→各論→結論の構成で、段落の冒頭が要点のトピックセンテンスになっている。1段落に1トピックが保たれ、段落は6文以下に収まっている。空行で区切りも適切。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff に示された変更と一致しており、ファイルパス・追加ロジックともに正確に反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`insertSubview`, `setContentScrollView`, `prefersLargeTitles` などの用語が正しく使用され、誤用は見られない。

説明の技術的正確性 ✓ PASS

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

技術的説明は PR の記述や Diff の内容と合致しており、因果関係も論理的に示されている。

事実の突合 ✓ PASS

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

全ての主張は PR タイトル、説明、Diff のコードに裏付けられており、外部知識や推測は含まれていない。

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

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

PR 番号 #199、追加されたテスト数「4 つ」等、数値情報は正確である。

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

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

記事タイトルは PR の趣旨(TabBarMinimize と prefersLargeTitle の挙動修正)を正確に表現している。

外部知識の正確性 ✓ PASS

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

記事内に PR に記載されていないバージョンサポート状況やリリース日程等の外部知識は含まれていない。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR が述べる「既に」や「将来」の表現と食い違う記述はない。