iOS9から使えるsafariのContentBlocker(広告ブロック)作る上で、hostアプリとextentionアプリでファイルやり取りする必要があったので実装したのですが、結構ハマりました。
ContentBlockerとかAppGroupとはなんぞや等はココが分かりやすかったです。
[公式]
https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html
[日本語]
http://www.toyship.org/archives/1845
ただ、ファイルやり取りしているサンプルや、概要説明がある記事があまりなかったです。(あんまりアプリ間ファイル共有しないのかなぁ...?)
とりあえずデータやり取りの動作確認が取れてるコードを乗せますが解決時の原因が分かってないところがあるので、記事の服用にはご注意を。
userdefaultの共有
まずは基本的なところとして、ちょっとしたUserの設定をHost側とExtention側で共有するとき。
// userdefaultに保存
func saveMessage(message: String, key: String) {
let userDefaults = UserDefaults(suiteName: groupID)
userDefaults?.set(message, forKey: key)
}
func loadMessage(key: String) -> String? {
let userDefaults = UserDefaults(suiteName: groupID)
return userDefaults?.object(forKey: key) as! String?
}
これはDataManager.swiftをhost側とextention側の両方にファイルを突っ込んでそれぞれsave, loadすればhost側とextention側でデータのやりとりが出来ます。
shared containerでfile共有
しかし、今回はそこそこの分量のjson(広告ブロックの定義ファイル)をやり取りしたかったので、user defaultじゃきついかなと思い、ファイルで保存して、そのファイルと共有したいなと思いました。
shared containerの共有ディレクトリ部分にファイル作成して、host とextentionから同じファイル参照すればいいかなと思い、こんな感じで書きました。
// ファイル名
let fileName = "hoge.json"
if let pathUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID)?.appendingPathComponent(fileName) {
try! jsonStr.write(to: pathUrl, atomically: true, encoding: String.Encoding.utf8)
}
しかし、こんなエラー吐きつつ落ちます。
file:///private/var/mobile/Containers/Shared/AppGroup/57283DD7~~~/hoge.json/
~ Is a Directory
と言われ、filePathがhoge.json/になってて、ディレクトリだよって言われて、書き込みできない...
// ファイル名
let fileName = "hoge"
if let pathUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID)?.appendingPathComponent(fileName) {
try! jsonStr.write(to: pathUrl, atomically: true, encoding: String.Encoding.utf8)
}
ファイル名から拡張子を取ったら書き込めるようになりました。
file:///private/var/mobile/Containers/Shared/AppGroup/57283DD7~~~/hoge
パスはこんな感じで、末尾にむしろ/が付かなくなった。
hogeファイルが、.txtなのか.jsonなのかわからず、拡張子は謎なんで嫌なんですが、.txtも上記同様ダメで、拡張子取ると書き込めてます。
ココがなぜなのかわかりません。
分かる方、コメントで助けていただきたいです。
host側で保存したファイルをextention側で読み取る
Extention側はユーザーの設定に応じて読み込むファイルを切り替えるという実装を行いました。
func beginRequest(with context: NSExtensionContext) {
let fileManager = FileManager.default
let ruleFilePath = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupID)?.appendingPathComponent(ruleFileName)
// Host App側でファイル作成されていればそっちのファイルを読む
if fileManager.fileExists(atPath: ruleFilePath!.path) {
self.loadUserRule(context: context, userRuleString: jsonStr)
} else {
// 初期設定の方を読む
self.loadDefaultRule(context: context)
}
}
デフォルトのblockerList.jsonを読むときは、NSItemProvider(contentsOf: ファイルリソース)で良いみたいだが、
func loadDefaultRule(context: NSExtensionContext) {
let attachment = NSItemProvider(contentsOf: Bundle.main.url(forResource: "blockerList", withExtension: "json"))!
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
func loadUserRule(context: NSExtensionContext, userRuleString: String) {
let fileManager = FileManager.default
let ruleFilePath = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupID)?.appendingPathComponent(ruleFileName)
let attachment = NSItemProvider(contentsOf: ruleFilePath!)!
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
だとうまく読み込めませんでした。
デバッグログ出ないので、なんでかはよく分かってませんが、NSItemProviderの作り方に原因があるっぽい。
今回保存した形式はtxtになってるので 中身をStringで読み込み、NSSecureCoding にキャストし、typeIdentifierを kUTTypeTextとして読み込んだのですがこちらもダメでした。
やはり、もとのblockerList.jsonはJSONなので、JSON形式でNSItemProviderを作る必要があるのかなと思い、String -> Data -> NSSecureCodingになおして、typeIdentifierはkUTTypeJSONで指定すると読めました。
func loadUserRule(context: NSExtensionContext, userRuleString: String) {
let data = userRuleString.data(using: String.Encoding.utf8)
let attachment = NSItemProvider.init(item: data as NSSecureCoding?, typeIdentifier: kUTTypeJSON as String)
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
余談ですが、typeIdentifierはめちゃめちゃ種類多いんですね...
画像を読み込むときなどはitemはData型で渡して、typeIdentifier はkUTTypePNGとかで読めるみたいです。
http://d.hatena.ne.jp/shu223/20140606/1402015233
とりあえず、これで広告ブロック用のjsonファイルのやり取りはできると思います。