如何在 iOS 上启用文件和文件夹权限
How to enable files and folders permission on iOS
我正在尝试使用 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 设备上!):
- 点击
Set downloads directory
按钮On my iPhone
- 单击
Save to downloads directory
按钮
- 发生错误
经过进一步调查,我发现 safari 使用 Files and Folders
隐私权限(位于 iPhone 上的 Settings > Privacy > Files and folders
中)访问应用程序沙箱外的文件夹(This link 对于我正在谈论的图像)。我尽可能多地搜索了网络,但找不到任何关于此确切权限的文档。
我见过非苹果应用程序(如 VLC)使用此权限,但我无法弄清楚它是如何授予的。
我尝试启用以下 plist 属性,但其中 none 有效(因为我后来意识到这些仅适用于 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 文档页面(在我发布此问题时 google 搜索了几个小时后没有找到)
https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
导航到文章的 Save the URL as a Bookmark
部分。
利用 SwiftUI fileImporter 可以一次性 read/write 访问用户选择的目录。要保留此 read/write 访问权限,我必须制作一个书签并将其存储在某个地方以便以后访问。
因为我只需要一个用户下载目录的书签,所以我把它保存在UserDefaults中(一个书签就大小而言是很小的)。
保存书签时,该应用程序会添加到用户设置的 Files and folders
中,因此用户可以立即撤销该应用程序的文件权限(因此我的代码片段中的所有保护语句)。
这是我使用的代码片段,经过测试,下载在应用程序启动和多次下载中持续存在。
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)")
}
}
}
}
我正在尝试使用 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 设备上!):
- 点击
Set downloads directory
按钮On my iPhone
- 单击
Save to downloads directory
按钮 - 发生错误
经过进一步调查,我发现 safari 使用 Files and Folders
隐私权限(位于 iPhone 上的 Settings > Privacy > Files and folders
中)访问应用程序沙箱外的文件夹(This link 对于我正在谈论的图像)。我尽可能多地搜索了网络,但找不到任何关于此确切权限的文档。
我见过非苹果应用程序(如 VLC)使用此权限,但我无法弄清楚它是如何授予的。
我尝试启用以下 plist 属性,但其中 none 有效(因为我后来意识到这些仅适用于 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 文档页面(在我发布此问题时 google 搜索了几个小时后没有找到)
https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
导航到文章的 Save the URL as a Bookmark
部分。
利用 SwiftUI fileImporter 可以一次性 read/write 访问用户选择的目录。要保留此 read/write 访问权限,我必须制作一个书签并将其存储在某个地方以便以后访问。
因为我只需要一个用户下载目录的书签,所以我把它保存在UserDefaults中(一个书签就大小而言是很小的)。
保存书签时,该应用程序会添加到用户设置的 Files and folders
中,因此用户可以立即撤销该应用程序的文件权限(因此我的代码片段中的所有保护语句)。
这是我使用的代码片段,经过测试,下载在应用程序启动和多次下载中持续存在。
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)")
}
}
}
}