WKWebViewを活用したSwiftとJavaScript連携の実装について
これは株式会社TimeTree Advent Calendar 2024の3日目の記事です。
こんにちは、TimeTreeでiOSアプリ開発をしていますNeganです。
はじめに
最近のリリースで、TimeTreeのiOSアプリに新しい画面が追加されました。この画面では、公開カレンダーのお得なイベントを簡単に見つけることができます。
おトクなイベント
当初、この画面を完全にネイティブで実装することがチーム内で検討されましたが、工数的なインパクトとリソースの効率化に加え、Webページの方が画面デザインの柔軟な変更が可能である点も考慮し、フロントエンドチームが開発したWebページをWKWebViewを通じて利用する方法をチームで選択しました。これにより、開発工数を大幅に削減しつつ、素早いUIの変更や調整が可能となりました。
しかし、単にWebページをWKWebViewで表示するだけでは仕様を満たすことはできません。Webページ上の操作をトリガーにネイティブアプリの機能・ロジックを呼び出したり、ネイティブアプリ側の情報をWebページに渡して動的に画面を切り替えたりするなど、双方向の連携が必要になります。つまり、SwiftとJavaScript間での通信を行う仕組みを構築する必要がありました。
この記事では、このWKWebViewを利用したネイティブアプリ(Swift)とWeb(JavaScript)間の連携方法について、サンプルコードを交えながら詳しく見ていきたいと思います。
サンプルコード例
フォロー状態のトグルボタンを題材にしたサンプルコードを交えて流れを見ていきましょう。
WKWebView側はローカルに立ち上げたWebページ(React App) のURL(http://localhost:3000/)を読み込みます。
画面は以下の通りです。
未フォロー状態 | フォロー状態 |
---|---|
フォロートグルボタンを押下すると、フォロー状態・アンフォロー状態を切り替えることができます。
サンプルコードの詳細は下記をご参照ください。
SwiftとJavaScriptの連携の仕組み
SwiftとJavaScriptを連携させるためには、WKWebViewを介した双方向通信の仕組みを構築する必要があります。基本的な流れとしては以下の2つがあります。
-
Swift → JavaScript
Swift側からJavaScriptの関数を呼び出し、Webページの動作を操作する方法です。今回のサンプルコードでは、フォロー完了時/フォロー失敗にPromiseを用いて非同期にJavaScriptへ通知を送る場面が該当します。 -
JavaScript → Swift
JavaScript側からネイティブコード(Swift)にメッセージを送り、処理を依頼する方法です。この記事の例では、ユーザーがフォロー/フォロー解除ボタンを押した際に、JavaScriptがSwiftにフォロー/フォロー解除のメッセージを送ります。
この双方向通信を円滑に行うには、WKUserContentController
を中心とした仕組みを正しく理解し、実装する必要があります。それではSwift側のコードを順に見ていきましょう。
WKWebViewの初期設定
まずは、WKWebView
の基本的なセットアップから始めます。WKWebView
は、Webコンテンツをアプリ内で表示するための強力なツールであり、WKWebViewConfiguration
を通じて細かい設定を行うことができます。この設定では、Webコンテンツの挙動やJavaScriptとの連携方法を指定します。
特に重要なのが、JavaScriptコードの注入を可能にするWKUserContentController
の設定です。このコントローラーを使用することで、SwiftとJavaScript間のメッセージ通信を構築したり、Webページの動作をカスタマイズしたりすることができます。
以下のサンプルコードでは、SwiftからJavaScriptの機能を追加するために、window.followToggleExample
オブジェクトを定義しています。このオブジェクトは、JavaScriptからSwiftにメッセージを送るためのエントリーポイントとなります。また、SwiftからJavaScriptへのメッセージ送信やログの受信も可能にしています。
// WebViewの設定
let config = WKWebViewConfiguration()
let contentController = WKUserContentController()
// JavaScriptコードを注入
let jsCode = """
window.followToggleExample = {
follow: (publicCalendarId) => {
return new Promise((resolve, reject) => {
window.__resolveFollowToggleCalendar = resolve;
window.__rejectFollowToggleCalendar = reject;
window.webkit.messageHandlers.follow.postMessage(publicCalendarId);
});
},
isFollowing: (options = {}) => {
return window.webkit.messageHandlers.isFollowing.postMessage(options);
}
};
"""
let userScript = WKUserScript(source: jsCode, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
contentController.addUserScript(userScript)
// Swift -> JavaScript のメッセージハンドラーを追加
contentController.add(self, name: "follow")
contentController.addScriptMessageHandler(LeakAvoidingScriptMessageHandlerWithReply(self), contentWorld: .page, name: "isFollowing")
config.userContentController = contentController
// JavaScript側のConsole.logを受ける
config.userContentController.add(self, name: "logging")
// override console.log
let _override = WKUserScript(source: "var console = { log: function(msg){window.webkit.messageHandlers.logging.postMessage(msg) }};", injectionTime: .atDocumentStart, forMainFrameOnly: true)
config.userContentController.addUserScript(_override)
webView = WKWebView(frame: self.view.bounds, configuration: config)
1. JavaScriptコードの注入
上記のコードでは、WKUserScript
を使用して、window.followToggleExample
というJavaScriptオブジェクトをWebページに注入しています。このオブジェクトを通じて、以下の機能が実現されます:
- follow メソッド:Swiftに対してフォローリクエストを送信します。Promiseで成功・失敗を通知します。(成功時にはresolve、失敗時にはrejectを呼び出します)
- isFollowing メソッド:現在のフォロー状態をSwiftに問い合わせ、結果を返します。
2. Swift -> JavaScriptの連携
contentController.addUserScript(userScript)
を使用して、JavaScriptコードをWebページの読み込み時に注入します。この設定によって、Webページ内でSwiftが定義したオブジェクトやメソッドが利用可能になります。
さらに、contentController.add(_:name:)
でメッセージハンドラーを登録することで、JavaScriptからSwiftにメッセージを送信できるようにしています。
3. JavaScriptのログ受信
console.logの出力をSwift側でキャッチするために、override console.log
を実装しています。この機能は、Webページのデバッグやログ確認に役立ちます。
4. evaluateJavaScriptの利用準備
このセットアップを通じて、SwiftからJavaScriptコードを実行するevaluateJavaScript
も利用可能になります。たとえば、Swift側から動的にJavaScript関数を呼び出し、Webページの動作を制御することができます。
JavaScriptからSwiftへのメッセージハンドリング
次に、JavaScriptから送られてくるメッセージを受け取り、それに応じた処理をSwift側で実行する流れを見ていきましょう。
以下のコードでは、followという名前のメッセージを受信した際に、受け取ったsampleIDをもとにフォロー処理を実行しています。
// JavaScriptからのメッセージを受信
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "follow", let sampleID = message.body as? String {
print("フォロー要求を受信: \(sampleID)")
Task {
do {
// フォロー/アンフォロー処理を実行
try await handleFollowToggle()
await sendJavaScriptMessage("window.__resolveFollowToggleCalendar()")
} catch {
await sendJavaScriptMessage("window.__rejectFollowToggleCalendar()")
}
}
} else if message.name == "logging" {
print("WebView: \(message.body) ")
}
}
// JavaScriptメッセージ送信を簡略化
private func sendJavaScriptMessage(_ script: String) async {
await MainActor.run {
self.webView.evaluateJavaScript(script) { _, error in
if let error = error {
print("JavaScript評価エラー: \(error)")
}
}
}
}
以下の通りevaluateJavaScript
を用いてJavaScriptのPromise
を解決しています。
- フォロートグル処理の成功時
window.__resolveFollowToggleCalendar()
- フォロートグル処理の失敗時
window.__rejectFollowToggleCalendar()
双方向通信の実現
双方向通信をさらに拡張するには、WKScriptMessageHandlerWithReply
を活用します。通常のWKScriptMessageHandler
では片方向通信(JavaScript→Swift)しかできませんが、この機能を使うことで、JavaScriptからSwiftに問い合わせを行い、それを受けたSwift側から何かしらの情報(今回のサンプルコードでは、フォロー状態を表現する isFollowed
)をJavaScript側へ非同期で渡すことが可能です。
func userContentController(
_ userContentController: WKUserContentController,
didReceive scriptMessage: WKScriptMessage,
replyHandler: @escaping (Any?, String?) -> Void
)
- scriptMessage: JavaScriptから送られてきたメッセージ。
- replyHandler: 処理結果をJavaScriptに返信するためのクロージャ。
サンプルコードでは、JavaScriptが現在のフォロー状態を問い合わせるisFollowing
関数を実装する場合、以下のコードのように処理を記述しています。
func userContentController(_ userContentController: WKUserContentController, didReceive scriptMessage: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
if scriptMessage.name == "isFollowing" {
replyHandler(self.isFollowed, nil)
} else {
replyHandler(nil, "Invalid message")
}
}
ここで、replyHandler
を用いてJavaScript側に結果を返しています。これにより、非同期通信がシンプルかつスムーズに行えるようになります。
Swiftから注入されたオブジェクトをJavaScript側で利用する
JavaScript側では、Swiftから注入されたwindow.followToggleExample
オブジェクトを活用します。このオブジェクトを通じて、Swiftにリクエストを送ります。
再掲となりますが下記のコード箇所です。
// JavaScriptコードを注入
let jsCode = """
window.followToggleExample = {
follow: function(publicCalendarId) {
return new Promise((resolve, reject) => {
window.__resolveFollowCalendar = resolve;
window.__rejectFollowCalendar = reject;
window.webkit.messageHandlers.follow.postMessage(publicCalendarId);
});
},
isFollowing: function(options) {
return window.webkit.messageHandlers.isFollowing.postMessage(options);
}
};
"""
declare global {
interface Window {
followToggleExample?: {
follow: (publicCalendarId: string) => Promise<void>;
isFollowing: () => boolean;
};
__resolveFollowCalendar?: () => void;
__rejectFollowCalendar?: () => void;
}
}
注入された window.followToggleExample
を用いて、フォロー機能を実装します。以下のJavaScriptコードでは、フォローボタン/フォロー解除ボタンを押すとfollow関数を呼び出し、フォローメッセージをSwift側に送信しています。
const onFollowToggleButtonClick = async () => {
try {
await window.followToggleExample?.follow("sample-id");
const following = await window.followToggleExample?.isFollowing();
setIsFollowed(!!following);
} catch (e) {
setIsFollowed(false);
}
};
このコードの流れを順に説明します。
-
フォローメッセージの送信:
window.followToggleExample?.follow("sample-id")
を呼び出し、sample-id
をSwift側に送信します。 -
フォロー状態の取得: フォロー処理が完了した後、
isFollowing
関数を呼び出して現在のフォロー状態を取得します。
このとき、Swift側では以下のコードが実行されます。
if scriptMessage.name == "isFollowing" {
replyHandler(self.isFollowed, nil)
}
- コンポーネントの再描画: 取得したフォロー状態に基づいて、コンポーネントの状態を更新します。
setIsFollowed(!!following)
によって、ボタンの表示が「フォローする」から「フォロー解除する」に切り替わります。
フォロー状態に応じてボタンのデザインとテキストを切り替えるReactコンポーネントを作成します。
const FollowToggleButton: React.FC<FollowToggleButtonProps> = ({ isFollowed, onClick }) => {
const buttonStyles = isFollowed
? "bg-red-500 hover:bg-red-600"
: "bg-blue-400 hover:bg-blue-600";
return (
<button
onClick={onClick}
className={`${buttonStyles} text-white px-6 py-2 rounded-full transition duration-300`}
>
{isFollowed ? "フォロー解除する" : "フォローする"}
</button>
);
};
まとめ
この記事では、SwiftとJavaScriptの双方向通信を利用して、iOSアプリで動的なWebページと連携する方法を紹介しました。
ポイントは以下の通りです。
-
WKWebView
の設定方法 - JavaScriptからSwiftにメッセージを送る方法
- 双方向通信を実現する
WKScriptMessageHandlerWithReply
の活用
最後に
このように、SwiftとJavaScriptの連携を活用することで、Webの柔軟性をネイティブアプリ側で最大限に活用することができます。ぜひ、実際のプロジェクトで試してみてください。
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion