cover

在 macOS 应用中使用 Bookmark 持久化对文件的访问权限

macOS 应用程序默认会开启 App Sandbox 应用沙盒,对于沙盒外的文件,需要用户主动授权才能进行访问。但是应用程序并不会主动保留对文件玩的访问权限,在应用重启后依旧需要再次授权才能访问。本文的主要内容是介绍如何通过 Bookmark 持久化对沙盒外的文件的访问权限。

2024-01-25

heading

BookmarkData 是什么?

URL
对象的实例方法,用于使用特定的选项为
URL
创建指向的目标资源创建书签数据。书签数据可以被保存到
UserDefaults
中或其他存储中,并在需要时还原为
URL
对象(即使用户将
URL
指向的资源进行了移动或重命名)。

heading

使用 BookmarkData 持久化文件的访问权限

使用前需要确保应用程序启用了对应用沙盒外文件的访问权限,开启步骤参考下面的截图:

开启访问权限

开启后根据下面的步骤持久化对文件夹的访问权限:

  1. 通过
    fileImporter
    读取目标文件夹并获取访问权限;
  2. 通过
    URL.bookmarkData(options:includingResourceValuesForKeys:relativeTo:)
    方法创建书签(启用
    withSecurityScope
    选项);
  3. 使用
    UserDefaults
    存储该书签数据;
  4. 应用程序重启后,从
    UserDefaults
    中读取
    BookmarkData
  5. 使用
    URL.init(resolvingBookmarkData:options:relativeTo:bookmarkDataIsStale:)
    方法将
    BookmarkData
    解析为
    URL
    (启用
    withSecurityScope
    选项);
  6. 如果
    bookmarkDataIsStale
    返回的布尔值为 true,则需要使用
    bookmarkData(options:includingResourceValuesForKeys:relativeTo:)
    重新创建书签,并更新应用程序中存储的书签版本;
  7. 在解析后的
    URL
    上调用
    startAccessingSecurityScopedResource()
    开始访问;
  8. 使用
    URL
    访问目标文件;
  9. 在解析后的
    URL
    上调用
    stopAccessingSecurityScopedResource()
    停止访问。

下面的是一个使用

BookmarkData
持久化文件夹访问权限的例子:

import SwiftUI struct BookmarkDataView: View { @State var target: URL? @State var selecting: Bool = false @State var files: [URL] = [] var body: some View { VStack { HStack { Text("目标路径" + (target?.path ?? "N/A")) .padding() Button("选择文件夹") { selecting.toggle() }.padding() } List(files, id: \.self) {file in Text(file.path) }.padding() } .onAppear { if let url = loadBookmarkURL() { scanTarget(at: url) target = url } } .fileImporter(isPresented: $selecting, allowedContentTypes: [.folder], allowsMultipleSelection: false, onCompletion: { do { let result = try $0.get() if result.first != nil { target = result.first! saveBookmark(target: result.first!) scanTarget(at: result.first!) } } catch { print("\(error)") } }) } // 保存 bookmarkData 到 UserDefaults func saveBookmark(target: URL) { do { let bookmarkData = try target.bookmarkData( options: [.withSecurityScope] ) UserDefaults.standard.set(bookmarkData, forKey: "bookmark") } catch { print("书签数据保存失败:\(error)") } } // 从 UserDefaults 中加载保存的 bookmarkData 并还原为文件路径 func loadBookmarkURL() -> URL? { if let bookmarkData = UserDefaults.standard.data(forKey: "bookmark") { do { var isStale = false let url = try URL( resolvingBookmarkData: bookmarkData, options: [.withoutUI, .withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale ) return url } catch { print("书签数据解析失败 \(error)") return nil } } return nil } func scanTarget(at url: URL) { let success = url.startAccessingSecurityScopedResource() if !success { print("加载失败") return } do { let fileManager = FileManager.default files = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) } catch { print("Error: \(error)") } url.stopAccessingSecurityScopedResource() } } #Preview { BookmarkDataView() }
BookmarkData.swift

在上面的代码中,点击“选择文件夹”按钮后会出现

fileImporter
文件选择器,选择并授权后会将选中文件夹中的文件保存到
files
中,然后渲染这些文件夹的名称列表。

此时关闭应用并重启后应用会直接读取

UserDefaults
中保存的
BookmarkData
数据然后完成渲染,期间不需要用户手动选择并授权。

完整的代码可以在 Github 上查看。