🧷

iOS の Safari Web ExtensionでAppとJacascriptで通信する方法

2022/01/14に公開

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では今の所その手段が用意されていません。
https://developer.apple.com/documentation/safariservices/safari_web_extensions/messaging_between_the_app_and_javascript_in_a_safari_web_extension

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から情報を受取る方法を紹介します。

content.js
browser.runtime.sendMessage({
    type: "content"
},
    function(response) {
    if (response.type == "native") {
        // ネイティブからメッセージを受け取った時に行いたい処理
    }
});
background.js
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;
});
SafariWebExtensionHandler.swift
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.jssendMessage() してそれを起点にsendNativeMessage()を実行し、 background.js で受け取ったレスポンスを content.jsに返すようにしています。

Discussion