アクションシートをトリガー要素上に表示

hotwired/hotwire-native-ios

iOS 26 以降、iPhone でも UIPopoverPresentationController が有効になり、メニューが他の UI と重なったりキャンセルボタンが欠如する問題を、クリックした要素の座標情報に基づいてメニューを表示するよう修正しました。

背景

このセクションの主張は、iOS 26 で従来のアクションシートが期待通りに表示されなかった背景を示すことです。 iOS 26 から iPhone でも popoverPresentationController が動作し、従来は iPad 限定だったポップオーバー機構が有効化されたため、MenuComponent が表示位置を固定(画面中央)したまま他メニューと重なるケースが頻発しました。さらに、UIAlertController のキャンセルボタンが描画されないという UI の不整合も報告されました。これらの症状は、デモアプリで確認されたスクリーンショット(iPhone iOS 26, iOS 18, iPad 各バージョン)に明示されています。結果として、ユーザーは期待するコンテキストメニューの位置付けが失われ、操作性が低下していました。

結論として、メニューの表示位置をトリガー要素に合わせることが必要でした。

技術的な変更

このセクションでは、MenuComponent に座標情報を受け取りポップオーバー位置を動的に設定する実装変更を解説します。 Demo/Bridge/MenuComponent.swiftWebKit のインポートを追加し、showAlertSheet のシグネチャを source: Source へ拡張しました。handleDisplayEvent からは data.source を渡すよう変更し、UIAlertControllerpopoverPresentationController に対して sourceViewsourceRect を設定します。sourceRect は Web 側から送られる Source 構造体(x, y, width, height)を利用し、メニューが正確にトリガー要素上に表示されます。

変更前後のコード比較です。

--- a/Demo/Bridge/MenuComponent.swift
+++ b/Demo/Bridge/MenuComponent.swift
@@
-import UIKit
+import UIKit
+import WebKit
@@
-        showAlertSheet(with: data.title, items: data.items)
+        showAlertSheet(with: data.title, items: data.items, source: data.source)
@@
-    private func showAlertSheet(with title: String, items: [Item]) {
+    private func showAlertSheet(with title: String, items: [Item], source: Source) {
@@
-        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
-        alertController.addAction(cancelAction)
-
-        // Set popoverController for iPads
-        if let popoverController = alertController.popoverPresentationController {
-            if let barButtonItem = viewController?.navigationItem.rightBarButtonItem {
-                popoverController.barButtonItem = barButtonItem
-            } else {
-                popoverController.sourceView = viewController?.view
-                popoverController.sourceRect = viewController?.view.bounds ?? .zero
-                popoverController.permittedArrowDirections = []
-            }
-        }
+        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
+        alertController.addAction(cancelAction)
+
+        // Set popoverController for devices that support them (iPad, iOS 26+)
+        if let popoverController = alertController.popoverPresentationController,
+           let vc = viewController as? Visitable,
+           let sourceView = viewController?.view,
+           let webView = vc.visitableView.webView {
+            popoverController.sourceView = sourceView
+            // The source coordinates come from the bridge component relative to the web page content.
+            popoverController.sourceRect = CGRect(x: source.x, y: source.y, width: source.width, height: source.height)
+            popoverController.permittedArrowDirections = []
+        }

この実装により、メニューはトリガー要素の矩形 (sourceRect) に合わせて表示され、iPhone でも iPad と同様のポップオーバー動作が得られます。 さらに、キャンセルアクションは従来通り追加され、UI の一貫性が回復しました。追加された WebKit インポートは WebView へのアクセスに必要であり、ブリッジ側の座標取得ロジックと整合します。

結論として、表示位置のデータ駆動化により、プラットフォーム差異を意識せず同一コードで正しい UI を提供できるようになりました。

設計判断

このセクションは、プラットフォーム固有の分岐を排除し、データ駆動でポップオーバー位置を決定する方針を説明します。 従来は UIDevice 判定で iPhone/iPad を分岐し、iPhone では位置を固定する回避策が議論されました(PR コメント参照)。しかし、iOS 26 以降の挙動変化に伴い、分岐ロジックは維持できず、根本的な解決策が求められました。

新方針は、Web 側から要素座標 (Source) を送信し、ネイティブ側がその情報だけでポップオーバーを配置することです。 これにより、OS バージョンやデバイス種別に依存しない一貫した処理が実現します。ブリッジコンポーネントは MessageDatasource フィールドを追加し、デモアプリ側の PR #105 で送信ロジックが実装されています。データが存在しない場合はデフォルトの中央表示がフォールバックします。

トレードオフとして、ブリッジ経由で座標情報を送るオーバーヘッドが増える点が挙げられますが、UI の正確性と一貫性を得るための合理的な代償と判断されました。 今回の設計は、将来的な UI コンポーネント拡張でも同様の座標駆動アプローチを再利用しやすくする基盤となります。

結論として、デバイス判定を排除しデータ駆動で位置決めを行う設計選択は、保守性と UI 整合性の向上に直結しています。

まとめ

本 PR は、iOS 26 以降に顕在化したアクションシートの位置ずれとキャンセルボタン欠如の問題を、トリガー要素の座標情報を利用して正確に表示するよう改修したものです。MenuComponentsource を受け取り popoverPresentationController に設定することで、iPhone と iPad の両方で期待通りの UI が提供され、プラットフォーム分岐を排除したシンプルな設計へと進化しました。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

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

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しく diff としてハイライトされていますが、ファイル名付きハイライトが使用されていません。GitHubリンクは「PR #196」の形式で記載されており、[#196](URL) の厳密な形式とは異なるため警告とします。

対象読者への適合性 ✓ PASS

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

技術的な内容が中心で、エンジニア向けに適切なレベルで記述されています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内の diff は提供された Diff と概ね一致し、ファイル名・変更箇所・追加コードが正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

UIPopoverPresentationController、UIAlertController、sourceRect などの用語が正しく使用されています。

説明の技術的正確性 ✓ PASS

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

iOS 26 での挙動変化やキャンセルボタン欠如、座標ベースの位置決めの説明は PR 内容と一致しています。

事実の突合 ✓ PASS

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

記事の主張は全て PR のタイトル、説明、Diff で裏付けられており、ハルシネーションは見当たりません。

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

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

PR番号 #196、iOS 26 といった数値・固有名詞は正確です。

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

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

記事タイトルは PR の趣旨「Position menu (action sheet) on element that triggered it」を適切に日本語化しています。

外部知識の正確性 ✓ PASS

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

外部知識(LTS、リリース日程等)は一切含まれていません。

時間表現の正確性 ✓ PASS

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

「iOS 26 以降」などの時間表現は PR の記述と一致しています。