SwiftUI中如何异步使用Transferable实现延迟下载的拖拽功能?
在SwiftUI中实现延迟下载的拖拽文件承诺(类似AppKit的NSFilePromiseProvider)
问题描述
希望将应用中代表网络驱动器文件的项目拖拽到用户桌面或Finder窗口,仅在拖拽操作结束后才下载文件。
在AppKit中可通过NSFilePromiseProvider实现该行为,但SwiftUI的Transferable或NSItemProvider方案存在局限:itemProvider闭包和FileRepresentation闭包会在拖拽会话启动时立即被调用,导致大文件或多文件拖拽时过早开始下载,影响使用体验。
原代码示例的核心问题:
struct Item: Identifiable, Transferable { let id = UUID() let name: String let description: String let url: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .fileURL) { (item: Item) in let urlSession = URLSession(configuration: .default) // 拖拽启动时立即触发下载 let (tempURL, response) = try! await urlSession.download(from: item.url) return SentTransferredFile(tempURL) } } } let items = [ Item(name: "Name 1", description: "Description 1", url: URL(string: "https://...")!), Item(name: "Name 2", description: "Description 2", url: URL(string: "https://...")!), Item(name: "Name 3", description: "Description 3", url: URL(string: "https://...")!) ] struct Test: View { var body: some View { Table { TableColumn("Column 1") { item in Text(item.name) } TableColumn("Column 2") { item in Text(item.description) } } rows: { ForEach(items) { item in TableRow(item) .itemProvider { let provider = NSItemProvider() provider.register(item) return provider } } } } }
解决方案:桥接AppKit的NSFilePromiseProvider
SwiftUI本身没有直接提供等效于NSFilePromiseProvider的API,但可以通过桥接AppKit的原生类实现延迟下载的需求,核心思路是:
- 自定义
NSFilePromiseProviderDelegate,在拖拽完成后的回调中执行下载逻辑 - 在SwiftUI的
itemProvider闭包中返回NSFilePromiseProvider实例,而非使用Transferable
步骤1:实现NSFilePromiseProviderDelegate
创建一个遵循NSFilePromiseProviderDelegate的类,负责处理文件承诺的写入逻辑,仅在拖拽结束后触发下载:
class FilePromiseDelegate: NSObject, NSFilePromiseProviderDelegate { private let item: Item init(item: Item) { self.item = item super.init() } // 返回承诺的文件类型和名称 func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String { return item.name } // 拖拽完成后执行的逻辑:下载文件并写入目标路径 func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) { let urlSession = URLSession(configuration: .default) let task = urlSession.downloadTask(with: item.url) { tempURL, response, error in guard let tempURL = tempURL, error == nil else { completionHandler(error) return } do { // 将临时下载文件移动到目标路径 if FileManager.default.fileExists(atPath: url.path) { try FileManager.default.removeItem(at: url) } try FileManager.default.moveItem(at: tempURL, to: url) completionHandler(nil) } catch { completionHandler(error) } } task.resume() } // 可选:处理拖拽取消的情况 func filePromiseProviderWasCancelled(_ filePromiseProvider: NSFilePromiseProvider) { // 可在此取消正在进行的下载任务 } }
步骤2:在SwiftUI中使用NSFilePromiseProvider
修改TableRow的itemProvider闭包,返回配置好的NSFilePromiseProvider实例:
struct Test: View { var body: some View { Table { TableColumn("Column 1") { item in Text(item.name) } TableColumn("Column 2") { item in Text(item.description) } } rows: { ForEach(items) { item in TableRow(item) .itemProvider { // 创建文件承诺提供者,指定文件类型和代理 let provider = NSFilePromiseProvider(fileType: kUTTypeFileURL as String, delegate: FilePromiseDelegate(item: item)) return provider } } } } } // 保留原Item结构(无需遵循Transferable) struct Item: Identifiable { let id = UUID() let name: String let description: String let url: URL } let items = [ Item(name: "Name 1", description: "Description 1", url: URL(string: "https://...")!), Item(name: "Name 2", description: "Description 2", url: URL(string: "https://...")!), Item(name: "Name 3", description: "Description 3", url: URL(string: "https://...")!) ]
关键说明
filePromiseProvider(_:writePromiseTo:completionHandler:)方法仅在用户完成拖拽(松开鼠标)后才会被调用,此时才启动文件下载- 下载完成后将临时文件移动到Finder指定的目标路径,并调用
completionHandler通知系统操作完成 - 如果拖拽被取消,
filePromiseProviderWasCancelled(_:)会被触发,可在此取消下载任务避免资源浪费
内容的提问来源于stack exchange,提问作者Yakuhzi
相关产品推荐
相关产品推荐

