如何使用Swift/Objective-C直接访问外部USB存储并写入文件
我之前刚好解决过完全一样的需求——直接在iOS应用里读写外部USB存储,不用绕临时文件的弯路。下面给你详细讲Swift的实现方案,Objective-C的思路完全一致,我也会附上关键代码:
UIDocumentPickerViewController获取外部存储的可写URL iOS的沙箱机制要求,应用不能直接访问外部存储,必须通过系统提供的文档选择器获取授权。通过UIDocumentPickerViewController,用户可以主动选择USB存储里的目标文件夹,我们就能拿到带有读写权限的URL,之后直接在该位置操作文件即可。
步骤1:配置Info.plist权限
首先需要在Info.plist里添加两个关键配置,确保应用支持原地操作外部文件:
- 添加
LSSupportsOpeningDocumentsInPlace并设置为YES:允许应用直接在外部存储位置读写文件,无需复制到沙箱。 - 添加
UIFileSharingEnabled并设置为YES(可选,但如果需要让用户通过iTunes共享文件的话建议加上)。
步骤2:弹出文档选择器,让用户选择USB存储的目标位置
下面是Swift代码示例,触发选择器让用户选择USB里的文件夹:
import UIKit class FileWriterViewController: UIViewController, UIDocumentPickerDelegate { // 调用这个方法弹出选择器(比如绑定到按钮点击事件) @IBAction func selectUSBStorageFolder(_ sender: UIButton) { // 创建选择器,允许选择文件夹并获得原地读写权限(asCopy: false是关键) let documentPicker = UIDocumentPickerViewController( forOpeningContentTypes: [.folder], asCopy: false ) documentPicker.delegate = self documentPicker.allowsMultipleSelection = false present(documentPicker, animated: true) } // 用户选择完成后的回调 func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let targetFolderURL = urls.first else { dismiss(animated: true) return } // 拿到目标文件夹URL后,直接执行写入操作 writeFileToTargetFolder(folderURL: targetFolderURL) dismiss(animated: true) } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { dismiss(animated: true) } }
步骤3:直接写入文件到USB存储
因为是外部存储,需要用NSFileCoordinator来协调文件操作,避免和其他应用的读写冲突。下面是写入文件的示例代码:
extension FileWriterViewController { func writeFileToTargetFolder(folderURL: URL) { // 示例:准备要写入的文件内容 let sampleContent = "这是写入USB存储的测试内容".data(using: .utf8)! // 拼接目标文件的完整URL(比如在选中的文件夹里创建test.txt) let targetFileURL = folderURL.appendingPathComponent("test.txt") let fileCoordinator = NSFileCoordinator() var coordinatorError: NSError? // 协调写入操作 fileCoordinator.coordinate( writingItemAt: targetFileURL, options: .forReplacing, // 如果文件已存在则替换 error: &coordinatorError ) { (safeFileURL) in do { // 直接写入到安全的URL(系统会处理权限和冲突) try sampleContent.write(to: safeFileURL, options: .atomic) DispatchQueue.main.async { print("文件成功写入USB存储!") // 这里可以给用户弹出成功提示 } } catch { DispatchQueue.main.async { print("写入失败:\(error.localizedDescription)") } } } if let error = coordinatorError { print("文件协调失败:\(error.localizedDescription)") } } }
Objective-C版本关键代码参考
如果你的项目用Objective-C,核心逻辑完全一致,下面是关键部分的代码:
#import "FileWriterViewController.h" @interface FileWriterViewController () <UIDocumentPickerDelegate> @end @implementation FileWriterViewController - (IBAction)selectUSBStorageFolder:(UIButton *)sender { UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithOpeningContentTypes:@[UTTypeFolder] asCopy:NO]; picker.delegate = self; picker.allowsMultipleSelection = NO; [self presentViewController:picker animated:YES completion:nil]; } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { NSURL *targetFolderURL = urls.firstObject; [self writeFileToTargetFolder:targetFolderURL]; [self dismissViewControllerAnimated:YES completion:nil]; } - (void)writeFileToTargetFolder:(NSURL *)folderURL { NSData *sampleContent = [@"这是写入USB存储的测试内容" dataUsingEncoding:NSUTF8StringEncoding]; NSURL *targetFileURL = [folderURL URLByAppendingPathComponent:@"test.txt"]; NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSError *coordinatorError = nil; [coordinator coordinateWritingItemAtURL:targetFileURL options:NSFileCoordinatorWritingOptionsForReplacing error:&coordinatorError byAccessor:^(NSURL *safeFileURL) { NSError *writeError = nil; [sampleContent writeToURL:safeFileURL options:NSDataWritingAtomic error:&writeError]; if (writeError) { NSLog(@"写入失败:%@", writeError.localizedDescription); } else { NSLog(@"文件成功写入USB存储!"); } }]; if (coordinatorError) { NSLog(@"文件协调失败:%@", coordinatorError.localizedDescription); } } @end
关键优化:持久化存储权限
用户每次选择文件夹太麻烦?可以把拿到的URL保存为书签(Bookmark),下次启动APP时直接恢复权限,不用再让用户选择:
// 保存书签到UserDefaults func saveBookmark(for url: URL) { do { let bookmarkData = try url.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) UserDefaults.standard.set(bookmarkData, forKey: "USBStorageBookmark") } catch { print("保存书签失败:\(error.localizedDescription)") } } // 从UserDefaults恢复书签并获取URL func restoreUSBStorageURL() -> URL? { guard let bookmarkData = UserDefaults.standard.data(forKey: "USBStorageBookmark") else { return nil } var isStale = false do { let url = try URL( resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale ) // 如果书签过期,需要重新让用户选择 if isStale { UserDefaults.standard.removeObject(forKey: "USBStorageBookmark") return nil } // 启动安全范围访问 if url.startAccessingSecurityScopedResource() { defer { url.stopAccessingSecurityScopedResource() } return url } } catch { print("恢复书签失败:\(error.localizedDescription)") } return nil }
注意:使用安全范围URL时,必须调用
startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource(),确保权限正确释放。
这个方案是苹果官方推荐的,完全符合iOS的安全规范,用户只需要选择一次目标文件夹(之后可以用书签记住),就能直接在APP内完成写入操作,体验比临时文件+Document Interaction Controller的方案流畅太多。
内容的提问来源于stack exchange,提问作者Federico Rotoli




