iOS の Safari Web ExtensionでAppとJacascriptで通信する方法
iOS15からSafariWebExtensionが追加され、iOSのSafariでも拡張機能の開発が可能となりました。
この拡張機能を開発するにあたって、SafariのWebViewに組み込まれるAppとJavascriptでの通信を行いたくなることがあります。
ドキュメントも用意されてるのですが、この通信を実現するにあたっていくつかハマったポイントがあったので、ハマったポイントとその解決策を紹介します。
App→Javascriptの通信
ドキュメントに書いてある通り browser.runtime.sendNativeMessage
を使うことで通信が可能です。
browser.runtime.sendNativeMessage("application.id", {message: "Hello"});
送られたメッセージは以下のようなSwiftコードで受信することが出来ます。
import SafariServices
import os.log
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey]
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
let response = NSExtensionItem()
response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
App→Javascriptの通信
アプリで設定した項目を拡張機能に反映させたい場合など、App→Javascriptの通信を行いたいケースは多そうです。
しかしドキュメントに記載がある通り、iOSでは今の所その手段が用意されていません。
You can’t send messages from a containing iOS app to your web extension’s JavaScript scripts.
ではAppの情報を拡張機能側に反映させることが不可能かというと、方法はあります。
この場合も先程と同じくbrowser.runtime.sendNativeMessage
を使います。
ここでは、Safariでコンテンツが読み込まれる時に実行される content.js
をトリガーに、Appから情報を受取る方法を紹介します。
browser.runtime.sendMessage({
type: "content"
},
function(response) {
if (response.type == "native") {
// ネイティブからメッセージを受け取った時に行いたい処理
}
});
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log("Received request: ", request);
if (request.type == "content") {
browser.runtime.sendNativeMessage("application.id", {message: "Hello"}, function(response) {
const obj = JSON.parse(response);
if (obj.type == "native") {
sendResponse(obj);
}
});
}
return true;
});
import SafariServices
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey]
let body: Dictionary<String, String> = ["type": "native"]
do {
let data = try JSONEncoder().encode(body)
let json = String(data: data, encoding: .utf8) ?? ""
let extensionItem = NSExtensionItem()
extensionItem.userInfo = [ SFExtensionMessageKey: json ]
context.completeRequest(returningItems: [extensionItem], completionHandler: nil)
} catch {
print("error")
}
}
}
この仕組みを行うための注意点があります。
sendNativeMessage()はbackground.jsで行う
sendNativeMessage()
の実行は content.js
でも可能ですが、レスポンスを受け取ることが出来ませんでした。
そこで今回紹介した方法では content.js
から background.js
に sendMessage()
してそれを起点にsendNativeMessage()
を実行し、 background.js
で受け取ったレスポンスを content.js
に返すようにしています。
Discussion