cover

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

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

2024-01-25

heading

BookmarkData 是什么?

bookmarkData(options:includingResourceValuesForKeys:relativeTo:)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 上查看。