ios - 如何在 iOS 上启用文件和文件夹权限

标签 ios swift alamofire

我正在尝试使用 AlamoFire 下载文件并将其保存到用户选择的下载目录(如 safari)。但是,每当我将下载目录设置为应用程序文档之外的文件夹时,都会出现以下错误(在真实的 iOS 设备上):

downloadedFileMoveFailed(error: Error Domain=NSCocoaErrorDomain Code=513 "“CFNetworkDownload_dlIcno.tmp” couldn’t be moved because you don’t have permission to access “Downloads”." UserInfo={NSSourceFilePathErrorKey=/private/var/mobile/Containers/Data/Application/A24D885A-1306-4CE4-9B15-952AF92B7E6C/tmp/CFNetworkDownload_dlIcno.tmp, NSUserStringVariant=(Move), NSDestinationFilePath=/private/var/mobile/Containers/Shared/AppGroup/E6303CBC-62A3-4206-9C84-E37041894DEC/File Provider Storage/Downloads/100MB.bin, NSFilePath=/private/var/mobile/Containers/Data/Application/A24D885A-1306-4CE4-9B15-952AF92B7E6C/tmp/CFNetworkDownload_dlIcno.tmp, NSUnderlyingError=0x281d045d0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}, source: file:///private/var/mobile/Containers/Data/Application/A24D885A-1306-4CE4-9B15-952AF92B7E6C/tmp/CFNetworkDownload_dlIcno.tmp, destination: file:///private/var/mobile/Containers/Shared/AppGroup/E6303CBC-62A3-4206-9C84-E37041894DEC/File%20Provider%20Storage/Downloads/100MB.bin)

该错误的摘要是我无权访问我刚刚授予访问权限的文件夹。

这是我附加的代码:

import SwiftUI
import UniformTypeIdentifiers
import Alamofire

struct ContentView: View {
    @AppStorage("downloadsDirectory") var downloadsDirectory = ""
    
    @State private var showFileImporter = false
    
    var body: some View {
        VStack {
            Button("Set downloads directory") {
                showFileImporter.toggle()
            }
            
            Button("Save to downloads directory") {
                Task {
                    do {
                        let destination: DownloadRequest.Destination = { _, response in
                            let documentsURL = URL(string: downloadsDirectory)!
                            let suggestedName = response.suggestedFilename ?? "unknown"

                            let fileURL = documentsURL.appendingPathComponent(suggestedName)

                            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
                        }

                        let _ = try await AF.download(URL(string: "https://i.imgur.com/zaVQDFJ.png")!, to: destination).serializingDownloadedFileURL().value
                    } catch {
                        print("Downloading error!: \(error)")
                    }
                }
            }
        }
        .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [UTType.folder]) { result in
            switch result {
            case .success(let url):
                downloadsDirectory = url.absoluteString
            case .failure(let error):
                print("Download picker error: \(error)")
            }
        }
    }
}

重现(在真实的 iOS 设备上运行!):

  1. 点击 Set downloads directory 按钮到 On my iPhone
  2. 单击保存到下载目录按钮
  3. 发生错误

经过进一步调查,我发现 safari 使用 文件和文件夹 隐私权限(位于 iPhone 上的 设置 > 隐私 > 文件和文件夹 中)访问外部文件夹应用程序沙箱(This link 是我所说的图像)。我尽可能多地搜索了网络,但找不到任何关于此确切权限的文档。

我见过非苹果应用程序(如 VLC)使用此权限,但我无法弄清楚它是如何授予的。

我尝试启用以下 plist 属性,但它们都不起作用(因为我后来意识到这些仅适用于 macOS)

<key>NSDocumentsFolderUsageDescription</key>
<string>App wants to access your documents folder</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>App wants to access your downloads folder</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>

有人可以帮我弄清楚如何授予文件和文件夹权限并解释它的作用吗?非常感谢您的帮助。

最佳答案

经过一些研究,我无意中发现了这个 Apple 文档页面(当我发布这个问题时,经过数小时的谷歌搜索后没有找到)

https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories

导航到文章的将 URL 另存为书签部分。

利用 SwiftUI fileImporter 提供对用户选择的目录的一次性读/写访问权限。为了保留这种读/写访问权限,我必须制作一个书签并将其存储在某个地方以备日后访问。

因为我只需要一个用户下载目录的书签,所以我将它保存在 UserDefaults 中(一个书签就大小而言非常小)。

保存书签时,该应用程序被添加到用户设置中的文件和文件夹中,因此用户可以立即撤销该应用程序的文件权限(因此我的代码片段中的所有保护语句).

这是我使用的代码片段,经过测试,下载在应用程序启动和多次下载中持续存在。

import SwiftUI
import UniformTypeIdentifiers
import Alamofire

struct ContentView: View {
    @AppStorage("bookmarkData") var downloadsBookmark: Data?
    
    @State private var showFileImporter = false
    
    var body: some View {
        VStack {
            Button("Set downloads directory") {
                showFileImporter.toggle()
            }
            
            Button("Save to downloads directory") {
                Task {
                    do {
                        let destination: DownloadRequest.Destination = { _, response in
                            // Save to a temp directory in app documents
                            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("Downloads")
                            let suggestedName = response.suggestedFilename ?? "unknown"

                            let fileURL = documentsURL.appendingPathComponent(suggestedName)

                            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
                        }

                        let tempUrl = try await AF.download(URL(string: "https://i.imgur.com/zaVQDFJ.png")!, to: destination).serializingDownloadedFileURL().value
                        
                        // Get the bookmark data from the AppStorage call
                        guard let bookmarkData = downloadsBookmark else {
                            return
                        }
                        var isStale = false
                        let downloadsUrl = try URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale)
                        
                        guard !isStale else {
                            // Log that the bookmark is stale
                            
                            return
                        }
                        
                        // Securely access the URL from the bookmark data
                        guard downloadsUrl.startAccessingSecurityScopedResource() else {
                            print("Can't access security scoped resource")
                            
                            return
                        }
                        
                        // We have to stop accessing the resource no matter what
                        defer { downloadsUrl.stopAccessingSecurityScopedResource() }
                        
                        do {
                            try FileManager.default.moveItem(at: tempUrl, to: downloadsUrl.appendingPathComponent(tempUrl.lastPathComponent))
                        } catch {
                            print("Move error: \(error)")
                        }
                    } catch {
                        print("Downloading error!: \(error)")
                    }
                }
            }
        }
        .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [UTType.folder]) { result in
            switch result {
            case .success(let url):
                // Securely access the URL to save a bookmark
                guard url.startAccessingSecurityScopedResource() else {
                    // Handle the failure here.
                    return
                }
                
                // We have to stop accessing the resource no matter what
                defer { url.stopAccessingSecurityScopedResource() }
                
                do {
                    // Make sure the bookmark is minimal!
                    downloadsBookmark = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil)
                } catch {
                    print("Bookmark error \(error)")
                }
            case .failure(let error):
                print("Importer error: \(error)")
            }
        }
    }
}

关于ios - 如何在 iOS 上启用文件和文件夹权限,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70750276/

相关文章:

ios - Swift:如何创建自定义 UINavigationBar 并添加自定义后退按钮?

ios - 有没有办法知道 iOS 的共享模型何时会出现或消失?

ios - Firebase pods 'InAppMessagingDisplay'

swift - 如何在 swift3 alamofire 中设置 Content-Type

ios - 尝试在 iOS 上使用 mailgun 发送电子邮件时出现错误 400

objective-c - 是否可以存储和检索使用 Objective-C 创建的对象? (在数据库中,用于 iOS 应用程序)

ios - 升级到 Xcode 7 后无法写入文档目录

ios - UISwitch 和 viewWithTag 在 numberOfRowsInSection 中为 nil

ios - 通过图像路径Alamofire获取图像

ios - 如何停止在并发线程中运行的预加载进程?