アクションシートをトリガー要素上に表示
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.swift に WebKit のインポートを追加し、showAlertSheet のシグネチャを source: Source へ拡張しました。handleDisplayEvent からは data.source を渡すよう変更し、UIAlertController の popoverPresentationController に対して sourceView と sourceRect を設定します。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 バージョンやデバイス種別に依存しない一貫した処理が実現します。ブリッジコンポーネントは MessageData に source フィールドを追加し、デモアプリ側の PR #105 で送信ロジックが実装されています。データが存在しない場合はデフォルトの中央表示がフォールバックします。
トレードオフとして、ブリッジ経由で座標情報を送るオーバーヘッドが増える点が挙げられますが、UI の正確性と一貫性を得るための合理的な代償と判断されました。 今回の設計は、将来的な UI コンポーネント拡張でも同様の座標駆動アプローチを再利用しやすくする基盤となります。
結論として、デバイス判定を排除しデータ駆動で位置決めを行う設計選択は、保守性と UI 整合性の向上に直結しています。
まとめ
本 PR は、iOS 26 以降に顕在化したアクションシートの位置ずれとキャンセルボタン欠如の問題を、トリガー要素の座標情報を利用して正確に表示するよう改修したものです。MenuComponent が source を受け取り popoverPresentationController に設定することで、iPhone と iPad の両方で期待通りの UI が提供され、プラットフォーム分岐を排除したシンプルな設計へと進化しました。