You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 17:45:46