Safariの拡張機能の作り方

# 前提
* Swift
* Xcode14.1
* iOS15以降(14以前はSafariが拡張機能に対応していない)


こんなアプリが作れます↓ぜひ使ってみてください！
> Safariでサイト上にメモをすることができるアプリ
> レシピサイトに、自分なりのメモをしたりできます。

https://apps.apple.com/tt/app/%E3%82%AF%E3%83%83%E3%82%AF%E3%83%A1%E3%83%A2-%E3%83%AC%E3%82%B7%E3%83%94-%E3%83%A1%E3%83%A2/id6444469776

# 用意
まず新規アプリを作ってみないと、ファイル構成は理解できないと思います。（僕はそうでした）
**必ず新規アプリを作ってから、次へ読み進んでください。**

### ・新規アプリを作る
Xcodeで新規アプリを作成
* File > New > Project

上のメニューから「**Multiplatform**」を選ぶ。
「**Safari Extension App**」を選んでファイルを作成

# Safari拡張機能の概要・ファイル構成
以下のようなファイル構成になっています。
**※使うものだけをリストアップします。**
* **Shared(Extension)**　- 拡張機能の制御
├ SafariWebExtensionHandler（拡張機能とアプリの橋渡し役）
├ Resources
　├ _locales
　　├ en
　　　├ messages（拡張機能の名前を設定）
　├ manifest（拡張機能の許可周り）
　├ background（SafariWebExtensionHandlerとの通信）
　├ content（Safariのサイト上に何かを表示するためのjs）
　├ popup.html（拡張機能のポップアップのHTML）
　├ popup.js（拡張機能のポップアップのjs）
　├ popup.css（拡張機能のcss）
* **iOS(App)** - アプリ側の制御
├ AppDelegate
├ SceneDelegate
├ Main

これを理解するのに3日かかりました笑

### popupとは？
![tutorial2-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280540/1d1f1465-4c93-378e-4c78-f578d3120cf5.jpeg)
ここを押すと...
![IMG_1064.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280540/55815fe4-42de-6827-9d93-c8426b1f6db2.png)
画面下部に表示されるこのViewのことです。




### まずは動かしてみる
#### 準備１
まずはSafari Extensionの動作を理解するために、簡単なアプリを作ってみましょう。
https://zenn.dev/h1d3mun3/articles/0bbcdcef81223c
このサイトが**非常にわかりやすい**ので、このサイトの最後までやってみてください。

#### 準備２
上記のサイトと同じことができたら、iOS(APP)ディレクトリにTopViewController.Swiftを作ってください。
「File -> New -> File -> Cocoa Touch Class -> UIViewController」です。
**※これが、Storyboardの初期ページのスクリプトになります。**

### どんな流れで動いている？
では、どのような流れでこのExtensionが動いているのか説明します。

まずは今のディレクトリを確認しましょう。
* **Shared(Extension)**　- 拡張機能の制御
├ SafariWebExtensionHandler（拡張機能とアプリの橋渡し役）
├ Resources
　├ _locales
　　├ en
　　　├ messages（拡張機能の名前を設定）
　├ manifest（拡張機能の許可周り）
　├ background（SafariWebExtensionHandlerとの通信）
　├ content（Safariのサイト上に何かを表示するためのjs）
　├ popup.html（拡張機能のポップアップのHTML）
　├ popup.js（拡張機能のポップアップのjs）
　├ popup.css（拡張機能のcss）
* **iOS(App)** - アプリ側の制御
├ AppDelegate
├ SceneDelegate
├ Main
├ TopViewController（アプリ画面の動作　通常のアプリと同じ）

#### 流れ
**TopViewController**
（データを保存）
↓
**UserDefaults(AppGroup)で保存**
（※AppGroupの使用が必須）
↑
**SafariWebExtensionHandler**
（Background.jsからのリクエストで、アプリ側のデータ（Userdefaultsのデータ）を取得して返す）
↑
**background.js**
（SafariWebExtensionHandlerに値をリクエストして、返り値をcontent.js等に返す）
↑↓
**content.js or popup.js**
（ユーザーに見える部分。拡張機能部分。
データが欲しいときは、background.jsにリクエストをする）

# コード一覧
### permission関係（manifest）
* 初期設定（まずは以下を設定してください）
```json:manifeset
"background": {
    "scripts": [ "background.js" ],
    "service_worker": "background.js",
    "persistent": false
}
```

* 特定のページで動作させる場合
```json:manifeset
"permissions": ["nativeMessaging"]
```

* 全てのサイトで動作させる場合
```json:manifeset
"permissions": ["nativeMessaging", "activeTab","<all_urls>"],
"content_scripts": [{
    "js": [ "content.js" ],
    "matches": [ "<all_urls>" ]
}]
```

### content.js → background.jsの通信
```js:content.js
browser.runtime.sendMessage({ key: "value" }).then((response) => {
    console.log("返り値は: ", response);

    switch(response.type) {
        case "message":
            console.log("返り値のTypeはmessageです");
            break;
    }
});
```

### popup.js → background.jsの通信
これも上記と同様です。


### background.jsでの処理
content.jsから値を受け取り、SafariWebExtensionHandlerに送り、返り値をcontent.jsに返す
こういうことです↓
> content.js → **background.js** → SafariWebExtensionHandler → **background.js** → content.js
```js:background.js
// content.jsからデータを取得
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
    console.log("typeは、",request["type"])
    
    // content.jsからのデータをそのまま、SafariWebExtensionHandlerに送る
    browser.runtime.sendNativeMessage("application.id", request, function(response) {
        // responseには、SafariWebExtensionHandlerからの返り値
        //その返り値を、content.jsに返す
        sendResponse({
            type: "message",
            data: {
                memo: response["memo"]
            }
        });
    });
});
```

### SafariWebExtensionHandler
最初から以下のような、 **「データを取得→background.jsに値を返す」** コードが書かれています。
コメントでコードを説明します。

```swift:SafariWebExtensionHandler
func beginRequest(with context: NSExtensionContext) 
    //background.jsから取得したデータ
    let item = context.inputItems[0] as! NSExtensionItem
    //valueを取り出す
    let message = item.userInfo?[SFExtensionMessageKey]
    os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)

    //※目印A

    //background.jsに返すデータを定義
    let response = NSExtensionItem()
    //返すデータを指定 「key:"文字列"」の形式
    response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
    //background.jsに返す
    context.completeRequest(returningItems: [response], completionHandler: nil)
}
```

* content.jsからのデータを取り出す
SafariWebExtensionHandlerで、content.jsからのデータを取得したいときは、上のコードの「**目印A**」の位置に以下を追加します。

```swift:SafariWebExtensionHandler
//content.jsからのデータを文字列に
var jsonString : String = String(format: "%@",message as! CVarArg)
// JSON文字列をData型に変換
var personalData: Data =  jsonString.data(using: String.Encoding.utf8)!
//key(今回はtype)を指定して、データを取得
var type : String = jsonData["type"] as! String
```

このような流れで、アプリ側とSafari Extensionとの通信することができました。

### popup.jsからcontent.jsへ通信する(番外編)
```js:popup.js
browser.tabs.query({ currentWindow: true, active: true }).then((tabs) => {
    //content.jsへ送信
    browser.tabs.sendMessage(tabs[0].id, {
        type: "changeContent",
        data: "data"
    }).then(handleResponse, handleError);
});
```

```js:content.js
//popup.jsからのデータを受け取る
browser.runtime.onMessage.addListener(handleMessage);
function handleMessage(request, sender, sendResponse) {
    //keyを指定してデータを取得
    console.log(request["data"]);

    //値をpopup.jsに返す（試していませんが、このコードで動くはずです...動かなかったらすみません）
    sendResponse({
        type: "message",
        data: {
            memo: response["memo"]
        }
    });
}
```

### popup.jsで開いているWebページのURLを取得する
ついでに、開いているページのタイトルも。
```js:popup.js
browser.tabs.query({ currentWindow: true, active: true }).then((tabs) => {
    let activeTabUrl = tabs[0].url;
    console.log("WebページのURLは、",activeTabUrl);

    let activeTabTitle = tabs[0].title;
    console.log("Webページのタイトルは、",activeTabTitle);
})
```

### 拡張機能の名前や説明を変更する
```json:messages
{
    "extension_name": {
        "message": "名前",
        "description": "The display name for the extension."
    },
    "extension_description": {
        "message": "説明です。",
        "description": "Description of what the extension does."
    }
}

```

### ログを確認する
* SafariWebExtensionHandlerのログの確認方法
```swift:SafariWebExtensionHandler
os_log( "値は: %{public}s", st)
```
ログの見方はこのページの、「ログの確認方法」をみてください。
https://techblog.recochoku.jp/8423#crayon-62de528f61a77688745988

* 拡張機能側のログの確認方法
```js:SafariWebExtensionHandler
console.log("ログ",st);
```
ログの見方は、このページを見てください。
https://qiita.com/unsoluble_sugar/items/2a3d06631a6b8259dc44

# まとめ
Safari Extensionは、仕組みがわかるまでは難しいですが、一度わかるとすぐに開発できると思うので、是非作ってみてくださいね！

### 紹介
Safari上でメモを書く＆表示できるアプリ「クックメモ」をリリースしました。
もちろんSafari拡張機能のアプリです！ぜひ使ってみてください〜↓↓↓

https://apps.apple.com/tt/app/%E3%82%AF%E3%83%83%E3%82%AF%E3%83%A1%E3%83%A2-%E3%83%AC%E3%82%B7%E3%83%94-%E3%83%A1%E3%83%A2/id6444469776