前提
- Swift
- Xcode14.1
- iOS15以降(14以前はSafariが拡張機能に対応していない)
こんなアプリが作れます↓ぜひ使ってみてください!
Safariでサイト上にメモをすることができるアプリ
レシピサイトに、自分なりのメモをしたりできます。
用意
まず新規アプリを作ってみないと、ファイル構成は理解できないと思います。(僕はそうでした)
必ず新規アプリを作ってから、次へ読み進んでください。
・新規アプリを作る
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とは?
ここを押すと...
画面下部に表示されるこのViewのことです。
まずは動かしてみる
準備1
まずはSafari Extensionの動作を理解するために、簡単なアプリを作ってみましょう。
https://zenn.dev/h1d3mun3/articles/0bbcdcef81223c
このサイトが非常にわかりやすいので、このサイトの最後までやってみてください。
準備2
上記のサイトと同じことができたら、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)
- 初期設定(まずは以下を設定してください)
"background": {
"scripts": [ "background.js" ],
"service_worker": "background.js",
"persistent": false
}
- 特定のページで動作させる場合
"permissions": ["nativeMessaging"]
- 全てのサイトで動作させる場合
"permissions": ["nativeMessaging", "activeTab","<all_urls>"],
"content_scripts": [{
"js": [ "content.js" ],
"matches": [ "<all_urls>" ]
}]
content.js → background.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
// 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に値を返す」 コードが書かれています。
コメントでコードを説明します。
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」の位置に以下を追加します。
//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へ通信する(番外編)
browser.tabs.query({ currentWindow: true, active: true }).then((tabs) => {
//content.jsへ送信
browser.tabs.sendMessage(tabs[0].id, {
type: "changeContent",
data: "data"
}).then(handleResponse, handleError);
});
//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を取得する
ついでに、開いているページのタイトルも。
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);
})
拡張機能の名前や説明を変更する
{
"extension_name": {
"message": "名前",
"description": "The display name for the extension."
},
"extension_description": {
"message": "説明です。",
"description": "Description of what the extension does."
}
}
ログを確認する
- SafariWebExtensionHandlerのログの確認方法
os_log( "値は: %{public}s", st)
ログの見方はこのページの、「ログの確認方法」をみてください。
https://techblog.recochoku.jp/8423#crayon-62de528f61a77688745988
- 拡張機能側のログの確認方法
console.log("ログ",st);
ログの見方は、このページを見てください。
https://qiita.com/unsoluble_sugar/items/2a3d06631a6b8259dc44
まとめ
Safari Extensionは、仕組みがわかるまでは難しいですが、一度わかるとすぐに開発できると思うので、是非作ってみてくださいね!
紹介
Safari上でメモを書く&表示できるアプリ「クックメモ」をリリースしました。
もちろんSafari拡張機能のアプリです!ぜひ使ってみてください〜↓↓↓