Security-Scoped Bookmarkが必要になった理由
Security-Scoped Bookmarkを使う前のバージョン
前バージョンでは、SMFPlayerアプリで再生するMIDIファイルをアプリ制限区域内にコピーしていました。
この制限区域というのは
/Users/ユーザ名/Library/Containers/アプリbundleID/Data/Documents
のことです。
このエリア内ではアプリが自由にリード/ライト、ファイルやフォルダの追加や削除ができます。
1台のMacで使うにはこれでいいでしょう。
アプリ制限内にコピーされたファイルにはどこかにオリジナルがあります。
また、設定にもよりますが、TimeMachineでバックアップをしていれば、そこからも復元可能です。
間違って消した場合はこれらから復旧すればいいので、「何でもあり」の編集を提供してました。
クラウドストレージを使って複数のMacで同じファイルを使いたい
SMFPlayerアプリを複数のMacで使う場合を考えてみます。
おおよそ、そういうユーザー(私も含めて)は複数のMacで同じファイルを扱いたいと思うはずです。
そのような場合にお手軽なのがクラウドストレージです。
私の場合はDropboxを利用しています。
AppStoreで配布するアプリはSandboxの利用が必須なので、アプリ制限区域外のフォルダ・ファイルを扱うには
- ユーザーによる明示的な指定
- ユーザーが指定したフォルダ・ファイルのアクセス権を維持する
が必要になります。
このアクセス権の維持で利用するのが「Security-Scoped Bookmark」です。
ユーザーによる明示的な指示
私の場合は、ファイルメニューに準備しました。
このメニューを選ぶと、オープンパネルが開き、そこで選択したフォルダの中にフォルダ・ファイルをSMFPlayerが利用します。
今回はフォルダだけを選択対象にしています。
let panel = NSOpenPanel()
panel.message = "Select a folder that contains MIDI files."
panel.allowsMultipleSelection = false
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.begin { [self] (result) in
if result == .OK {
if let url = panel.url {
//urlがユーザーが明示的に指定したフォルダ
}
}
ユーザーが指定したフォルダのブックマークを作成する
ブックマークはData型のデータです。
取得したurlからブックマークを作成します。
var bookmark: Data?
do {
bookmark = try url.bookmarkData(options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil)
} catch {
print("bookmark生成に失敗しました")
}
ブックマークを保存する
UserDefaultsを使って保存しておきます。
UserDefaults.standard.setValue(bookmark, forKey: "BOOKMARK")
UserDefaults.standard.synchronize()
次回のアプリ起動時、このbookmarkを使って利用するurlを復元します。
ブックマークからURLを復元する
まずはUserDefaultsでブックマークを取り出します。
bookmark = UserDefaults.standard.data(forKey: "BOOKMARK")
次にbookmarkを使ってurlを復元します
do {
var isStale = false
let url = try URL(resolvingBookmarkData: bookmark!,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale)
} catch {
}
ここで取得したurlが「ユーザーが明示的に指定したフォルダ」です。
詳細は割愛しますが、このbookmarkはフォルダをFinderで移動した場合でも有効です。
しかし、Finderで移動した場合はURLが変わっているので、bookmarkを修正しなくてはいけません。
これはこのURLの復元の時に「isStale」を確認するとわかります。
urlを使用〜そのままでは使えない
取得したのは単なるURLなので、そのままアクセスしようとすると、当然アクセス権がないためアクセスを拒否されます。
bookmarkから復元したURLは下記のようにセキュリティスコープを利用します。
if url.startAccessingSecurityScopedResource() {
defer {url.stopAccessingSecurityScopedResource()}
dprint("セキュリティスコープアクセスに成功しました")
//ここにファイル編集などの処理を記述する
} else {
dprint("セキュリティスコープアクセスに対応していないファイルです")
}
一連の処理をURLのextensionにすると便利
これでSandboxを利用していてもユーザーが明示的に指定したフォルダ・ファイルであれば、自由にアクセスが可能です。
あとはこれらの処理をなるべく少ない記述でできるよう、URLのextensionに追加するとより使いやすくなります。
bookmarkの生成
生成時のオプションは毎回決まっているので、一発で生成したいですね。
public var bookmark: Data? {
var bookmark: Data?
do {
bookmark = try bookmarkData(options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil)
} catch {
dprint("bookmark生成に失敗しました:\(path(percentEncoded: false).lastPathComponent)")
}
return bookmark
}
bookmarkからURLの復元
私はURLのextensionにこのように追加しました。
指定したフォルダ・ファイルがFinderで移動されていた場合の対応もしています。
戻り値のnewBookmarkがnilでない場合、newBookmarkを再度UserDefaultsで保存します。
実際のところ、newBookmarkがnilでなくて再保存しなくても使えています...この辺はもう少し調べないといけませんね。
///bookmarkが更新されたら、newBookmarkにDataが入っています
///bookmarkが更新されていなければ、newBookmarkはnilです
public static func fromBookmark(_ bookmark: Data?) -> (url: URL?, newBookmark: Data?) {
if bookmark == nil {
return (nil, nil)
} else {
var result: (url: URL?, newBookmark: Data?)
var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmark!,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale)
result.url = url
if isStale { //bookmark情報を更新すべきかもしれない
if url.startAccessingSecurityScopedResource() {
defer {url.stopAccessingSecurityScopedResource()}
let newBookmark = try url.bookmarkData(options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil)
//作ったばかりだから失敗しないはず(isStaleはチェックしない)...新しいURLを返す
result.url = try! URL(resolvingBookmarkData: newBookmark, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
result.newBookmark = newBookmark
}
}
} catch {
}
return result
}
}
セキュリティスコープアクセス
これをURLのextensionに追加しました。
使う時はクロージャーの中にファイルアクセスの処理だけを書けばいいので便利です。
いちいちstartAccessing...を記述しなくてもいいし、stopAccessingSecurityScopedResource()の呼び忘れ防止にもなります。
///bookmarkで保存された(セキュリティスコープアクセス)URLの操作をするためのクロージャです
public func withSecurityScopedAccess(_ block: () -> Void) {
if isInContainer {
block()
} else {
if startAccessingSecurityScopedResource() {
defer {stopAccessingSecurityScopedResource()}
print("セキュリティスコープアクセスに成功しました:\(path(percentEncoded: false).lastPathComponent)")
block()
} else {
print("セキュリティスコープアクセスに対応していないファイルです:\(path(percentEncoded: false).lastPathComponent)")
}
}
}
最後に
このブックマーク、セキュリティスコープを使えば、今までUserDefaultsで保存していたものを書類フォルダなどに保存することができます。
また、クラウドストレージを保存先に指定すれば、複数のMacで同じファイルを参照することができるので、とっても便利になります。
皆さんのアプリ開発の参考になれば嬉しいです。