webViewでswiftUI viewを表示する
iOSアプリ開発において、「WebViewの中にネイティブなSwiftUIviewを重ねて表示したい」という要件が出てくることもあります。
この記事では、WKWebViewの中にSwiftUIビューを追加する方法を紹介し、特に「Webコンテンツの末尾にSwiftUI製のフッターを追加する」ユースケースに焦点を当てます。
目標
・WKWebViewで表示されるWebページの末尾に、SwiftUIで作成したフッターを追加表示します。
・フッターの高さは動的に指定可能
・Webの内容が短くても、無駄な空白が生まれないよう調整
・Webページ側にマーカー(app-footer-placeholder)を設置することで、フッターの位置を決定
追加する方法
方法1
WKWebView の scrollView に SwiftUI View を直接追加する
メリット | デメリット |
---|---|
Web 側の任意の場所に View を追加できる(たとえば中間や末尾など)という柔軟性 | Webとの連携が必要、app-footer-placeholder を Web 側に埋め込む必要がある |
WebView 高さに依存しない、WebView 自体はフルサイズで固定なので、高さ調整が不要 | JavaScript依存、getBoundingClientRect() などで位置を取得しないとズレる可能性あり |
SwiftUI View を後から差し替え可能、scrollView に乗っているので表示更新も容易 | デバッグがやや難しい、レイアウトのミスマッチが起きたとき、原因がWebかSwiftUIか判断しにくい |
方法2
親の ScrollView に WebView と SwiftUI View を順に配置する
ScrollView {
WebView()
FooterView()
}
メリット | デメリット |
---|---|
SwiftUIのレイアウト構造にフィットしやすく、設計が明快 | WebViewの高さ測定が難しい、正確な高さを取得するにはJavaScript + evaluateJavaScript が必要 |
更新しやすい、SwiftUI View の差し替えや表示条件が簡単に書ける | レイアウトが崩れやすい、高さがズレると、WebViewの一部がスクロールできなくなることがある |
デバッグしやすい、レイアウトの流れがSwiftUIだけで完結するため、調整が楽 | WebViewが読み込み完了してからでないとViewを確定できないため、表示タイミング制御が難しい |
方法1を採用しました。方法1を選んだ理由は、
・WebView 本来のスクロールがそのまま使える
・Webコンテンツの任意の場所に SwiftUI View を挿入できる
・Webページがどんなに長くても対応できる
・非同期にUIを追加・制御しやすい
・Webとアプリのハイブリッド設計に向いている
実装の概要
フッター表示の主な流れは以下の通りです:
・WKWebViewが読み込みを完了したタイミングで、JavaScriptを使ってスペーサーを注入(主にiOS側での描画調整用)。
・Webページ内に存在するマーカー(例: id="app-footer-placeholder")の位置を取得。
・SwiftUIで作成されたviewを、その位置に合わせて scrollView 上に追加。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let footerHeight: CGFloat = parent.footerHeight
let injectScript = """
(function() {
const existing = document.getElementById('ios-footer-placeholder');
if (!existing) {
const spacer = document.createElement('div');
spacer.id = 'ios-footer-placeholder';
spacer.style.height = '\(footerHeight)px';
spacer.style.background = 'transparent';
spacer.style.pointerEvents = 'none';
document.body.appendChild(spacer);
}
})();
"""
webView.callAsyncJavaScript(injectScript, in: nil, in: WKContentWorld.defaultClient, completionHandler: nil)
let getFooterYScript = """
(function() {
const el = document.getElementById("app-footer-placeholder");
if (!el) return null;
const rect = el.getBoundingClientRect();
return rect.top + window.scrollY;
})()
"""
webView.evaluateJavaScript(getFooterYScript) { [weak self] result, error in
if let y = result as? CGFloat,
let self {
if let footerHostingView {
let scrollView = webView.scrollView
scrollView.addSubview(footerHostingView)
scrollView.contentSize.height += footerHeight
} else {
let hosting = hostingController
let scrollView = webView.scrollView
hosting.view.frame = CGRect(
x: 0,
y: y,
width: scrollView.frame.width,
height: footerHeight
)
scrollView.addSubview(hosting.view)
scrollView.contentSize.height += footerHeight
footerHostingView = hosting.view
}
}
}
}
注意点
1 Webページ側にapp-footer-placeholderを用意する必要がある
この実装では、Webページにあらかじめ以下のようなHTML要素が配置されている必要があります:
<div id="app-footer-placeholder"></div>
この要素の位置を基準にフッターの表示位置を決めているため、Web側との事前連携が重要です。
2 Webの内容が短い場合の対策
内容が少なく、スクロール可能な範囲が狭いWebページでは、フッターが画面内に食い込んでしまうことがあります。これを防ぐため、iOS側でスペーサー(透明のDIV)を挿入し、最低限の高さを確保しています。

株式会社SKIYAKIのテックブログです。ファンクラブプラットフォームBitfanの開発・運用にまつわる知見や調べたことなどを発信します。主な技術スタックは Ruby on Rails / React / AWS / Swift / Kotlin などです。 recruit.skiyaki.com/
Discussion