WKWebViewのJSとNativeの連携で通常の関数呼び出しのようにNative処理の結果を受け取る
WKWebView
でJavaScriptからNative側を呼び出す方法は検索すると出てきますが、ネイティブ側の処理結果を同じコンテキストで受け取る方法の情報が少なかったのでメモしておきます。
要点
-
WKScriptMessageHandler
を使った方法ではJavaScript側からの呼び出しに対して同じコンテキストで戻り値が返せない - JavaScriptの
prompt()
関数をWKUIDelegate
プロトコルで上書きすると通常の関数呼び出しのように同じコンテキストで処理結果を返すことができる
実現したいこと
JavaScript上で次のようなコードを記述したい。
var result = nativeMethod() // nativeMethodはネイティブ側(Swift)で用意した処理
alert(result) // 後続の処理でネイティブの処理結果(戻り値)を使う
nativeMethod()
の処理結果を同期的に取得する必要があるので、JavaScript側はnativeMethod処理が終わるのを待っている必要があります。ここでは諸事情でPromise
やasync/await
を使用しないことを想定しています。
JavaScript側からNative側を呼び出す一般的な方法(webkitのmessageHandlersとevaluateJavaScriptを組み合わせた非同期処理)
WKWebViewでJavaScriptからNative側を呼び出す方法を検索するとwebkit
のmessageHandlers
を使った実装例が出てきますが、この方法は戻りを返すことができません。WKWebView
のevaluateJavaScript
を使ってNative側からJavaScriptコードを実行することで結果を返すことができます。結果的にJavaScritpからの呼び出しと戻り値が別々のコンテキスト(非同期処理)になります。
Native側(Swift)のコードの例です。
var webView: WKWebView!
let handlerName = "nativeBridge" // JavaScript側に公開されるメッセージハンドラの名前
// WKWebViewを初期化するコード
private func setupWebView() {
let userContentController: WKUserContentController = WKUserContentController()
userContentController.add(self, name: handlerName) // メッセージハンドラの名前をセット
let configuration: WKWebViewConfiguration = WKWebViewConfiguration()
configuration.userContentController = userContentController // メッセージハンドラをセットしたuserContentControllerを代入
// configurationを使ってWKWebViewを生成する
webView = WKWebView(frame: .zero, configuration: configuration)
}
// JavaScriptからの呼び出しを処理するコード
extension ViewController: WKScriptMessageHandler {
// JS -> Native の呼び出し
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == handlerName) {
print(message.body)
evalJavaScript()
}
}
// Native -> JS の呼び出し
private func evalJavaScript() {
let message = "asynchronus message."
let executeScript: String = "callFromNative(\"\(message)\");" // callFromNative() というコードをevalする
webView.evaluateJavaScript(executeScript, completionHandler: { (object, error) -> Void in
if let object = object {
print(object)
}
if let error = error {
print(error)
}
})
}
}
Native側を呼び出して、処理の結果を取得するJavaScript側のコード例です。
// Nativeを実行する関数
function callAsynchronusNativeBridge() {
var object = {
"foo": "bar"
}
webkit.messageHandlers.nativeBridge.postMessage(object); // nativeBridgeという名前空間を使ってSwift側を呼び出す
}
// Nativeから呼び出される関数
function callFromNative(value) {
var element = document.getElementById("asynchronusResult");
element.value = value;
}
callAsynchronusNativeBridge
とcallFromNative
は異なるコンテキストで実行されるのでNative側の呼び出しと戻り値の取得(処理)が非同期になります。
prompt()
関数をWKUIDelegate
プロトコルで上書きして処理の結果を返す(同期処理)
JavaScriptのprompt()
関数とは下記のようにWebページでテキスト入力のポップアップを表示する関数です。
JavaScriptでは以下のように使います。
var result = prompt("any message") // ポップアップを表示してユーザーの入力を待つ
alert(result) // resultには入力されたテキストが入っている
この関数はWKWebView
上のコンテンツで実行しても通常は上記のようなポップアップが表示されません。WKWebView
ではprompt()
関数の呼び出しのハンドリングはNative側に委ねられています。そのためにWKUIDelegate
にrunJavaScriptTextInputPanelWithPrompt
で始まるメソッドが用意されています。
optional func webView(_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void)
この関数はJavaScriptでprompt()
を呼び出すと実行され、completionHandler
にString
をセットして文字列を返します。JavaScript側はNative側がデータを返すまで処理を待っています。
実際のコードの例は次の通りです、ここではJSONを使った JS -> Native でのやりとりを想定した実装にしました。
Native側のコードです。
extension ViewController: WKUIDelegate {
// JSでprompt()が呼び出されたときに実行される関数
func webView(_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void) {
// decode json stringify text.
let jsonText = prompt.data(using: .utf8)!
let json = try! JSONDecoder().decode([String : String].self, from: jsonText)
// json["foo"] --> bar
// return json stringify text.
let dict = ["body" : "synchronus message."]
let data = try! JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
let text = String(data: data, encoding: String.Encoding.utf8) ?? ""
// completeHandler を呼び出して戻り値を返す
completionHandler(text)
}
}
JavaScript側のコードです。
// JS -> Native (Synchronus)
function callSynchronusNativeBridge() {
var object = {
"foo": "bar"
}
var result = prompt(JSON.stringify(object)) // Native側でWKUIDelegateの関数が呼び出される
// resultにはNative側のcompleteHandlerでセットした値が入っている
var element = document.getElementById("synchronusResult");
element.value = JSON.parse(result).body
}
上記のコードではcallSynchronusNativeBridge
の中でprompt()
を呼び出してNative側を呼び出しています。Native側ではWKUIDelegate
の関数が呼び出されるので処理の結果をcompletionHandler
にセットします。
completionHandler
にセットされた値がprompt()
関数の戻り値として返却されます。
サンプルコード
簡単に試せるサンプルコードをGitHubに置いておきます。
Discussion