如何在iOS Files应用中实现跨应用重启的文件夹读写操作
如何在iOS Files应用中实现跨应用重启的文件夹读写操作
嘿,我刚好处理过类似的需求,给你梳理下完整的实现步骤——完全不用依赖Dropbox、Google Drive那些付费API,全靠iOS系统自带的文件选择器和权限持久化能力搞定,完美适配你的免费SwiftUI应用场景。
核心思路
默认情况下,用UIDocumentPickerViewController选中文件夹后,APP的访问权限是临时的,重启后就失效了。所以关键是用Bookmark机制持久化保存文件夹的访问权限,重启APP后通过Bookmark恢复访问能力。
步骤1:用SwiftUI包装UIDocumentPickerViewController
因为SwiftUI没有原生的文件夹选择器,我们需要把UIKit的UIDocumentPickerViewController包装成SwiftUI可用的组件,并且设置正确的参数来请求文件夹权限:
import SwiftUI import UIKit import MobileCoreServices struct FolderPicker: UIViewControllerRepresentable { typealias UIViewControllerType = UIDocumentPickerViewController func makeUIViewController(context: Context) -> UIDocumentPickerViewController { // 指定选择类型为文件夹 let picker = UIDocumentPickerViewController( documentTypes: [kUTTypeFolder as String], in: .open ) picker.allowsMultipleSelection = false picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator() } class Coordinator: NSObject, UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let selectedFolderURL = urls.first else { return } // 处理选中后的权限持久化 do { // 生成带安全范围的Bookmark数据 let bookmarkData = try selectedFolderURL.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) // 把Bookmark存在Keychain里(比UserDefaults安全,避免泄露或被备份) KeychainHelper.save(data: bookmarkData, forKey: "SyncedFolderBookmark") print("文件夹权限已保存") } catch { print("保存权限失败: \(error.localizedDescription)") } } } }
步骤2:实现Keychain存储工具
为了安全地保存Bookmark数据,我们写一个简单的Keychain工具类(别用UserDefaults,它会被iCloud备份且安全性差):
import Foundation class KeychainHelper { static func save(data: Data, forKey key: String) { // 先删除旧的记录(如果存在) let deleteQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key ] SecItemDelete(deleteQuery as CFDictionary) // 保存新的Bookmark数据 let saveQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] let status = SecItemAdd(saveQuery as CFDictionary, nil) if status != errSecSuccess { print("Keychain保存失败,状态码: \(status)") } } static func loadData(forKey key: String) -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: kCFBooleanTrue!, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) return status == errSecSuccess ? (result as? Data) : nil } }
步骤3:APP重启后恢复文件夹访问权限
每次APP启动时,从Keychain取出Bookmark数据,解析成可访问的URL,恢复权限:
func restoreSyncedFolder() -> URL? { guard let bookmarkData = KeychainHelper.loadData(forKey: "SyncedFolderBookmark") else { print("没有找到保存的文件夹权限") return nil } var isBookmarkStale = false do { let folderURL = try URL( resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isBookmarkStale ) // 如果Bookmark过期了,重新保存一份新的 if isBookmarkStale { let newBookmarkData = try folderURL.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) KeychainHelper.save(data: newBookmarkData, forKey: "SyncedFolderBookmark") } return folderURL } catch { print("恢复文件夹权限失败: \(error.localizedDescription)") return nil } }
步骤4:读写文件夹内的文件(必须启用安全范围)
每次操作文件夹里的文件时,都必须用startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()包裹操作——这是iOS的安全要求,否则会读写失败:
func writeTestFileToSyncedFolder() { guard let folderURL = restoreSyncedFolder() else { // 提示用户重新选择文件夹 print("请先选择同步文件夹") return } // 启用安全范围访问 guard folderURL.startAccessingSecurityScopedResource() else { print("无法访问文件夹") return } // 用defer确保不管成功失败都会停止访问 defer { folderURL.stopAccessingSecurityScopedResource() } // 执行文件读写操作 let testFileURL = folderURL.appendingPathComponent("同步测试文件.txt") do { try "这是来自你的SwiftUI应用的同步内容".write(to: testFileURL, atomically: true, encoding: .utf8) print("文件写入成功") } catch { print("文件写入失败: \(error.localizedDescription)") } }
步骤5:在SwiftUI视图中调用选择器
最后,在你的SwiftUI界面里添加按钮,触发文件夹选择器:
struct ContentView: View { @State private var showFolderPicker = false var body: some View { VStack(spacing: 20) { Button("选择同步文件夹") { showFolderPicker = true } Button("写入测试文件") { writeTestFileToSyncedFolder() } } .sheet(isPresented: $showFolderPicker) { FolderPicker() } .padding() } }
关键注意事项
- 不要用UserDefaults存储Bookmark数据:UserDefaults会被iCloud备份,安全性低,Keychain是更合适的选择。
- 必须配对调用安全范围方法:
startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()一定要配对,用defer可以保证即使代码抛出异常也会停止访问。 - 处理Bookmark过期:有些情况下Bookmark会失效(比如用户在Files应用里移动了文件夹),所以恢复时要检查
isBookmarkStale,及时更新Bookmark。
备注:内容来源于stack exchange,提问作者Enric
相关产品推荐
相关产品推荐

