如何正确注册NSItemProvider的可失败加载处理器?解决拖拽重试时loadHandler回调不触发问题
如何正确注册NSItemProvider的可失败加载处理器?解决拖拽重试时loadHandler回调不触发问题
我之前也碰到过这个坑!这其实是系统的缓存机制在搞鬼:当你通过NSItemProvider注册的加载操作返回失败(不管是传nil URL还是带错误信息),系统会把这个失败状态缓存下来——只要后续拖拽的目标目录和suggestedName和之前失败的那次一致,系统就会直接跳过loadHandler,不再执行。
下面给你几个亲测有效的解决办法,你可以根据自己的场景选择:
方法1:给suggestedName加唯一标识绕缓存
每次生成NSItemProvider时,给文件名加个临时的唯一后缀(比如时间戳、短UUID),让系统误以为是新的资源,这样就不会复用之前的失败缓存。等加载成功后,再确保实际传递的文件是原始名称就行:
SomeSwiftUIView() .itemProvider { let provider = NSItemProvider() // 加个时间戳后缀,保证每次的suggestedName都不一样 let tempSuggestedName = "\(fileName)_\(Int(Date().timeIntervalSince1970))" provider.suggestedName = tempSuggestedName provider.registerFileRepresentation(for: .data) { completionHandler in do { let fileURL = try someThrowableOperationToGetTheFileURL() // 把文件重命名回原始名称(如果需要的话) let finalURL = fileURL.deletingLastPathComponent().appendingPathComponent(fileName) if !FileManager.default.fileExists(atPath: finalURL.path) { try FileManager.default.moveItem(at: fileURL, to: finalURL) } completionHandler(finalURL, false, nil) } catch { // 即使传错误,因为suggestedName是新的,下次重试也会触发回调 completionHandler(nil, false, error) } return nil } return provider }
方法2:改用数据表示代替文件表示
如果你的文件不是特别大,可以试试registerDataRepresentation直接传文件数据,而不是文件URL。这种方式的缓存逻辑更宽松,每次拖拽都会触发加载回调:
SomeSwiftUIView() .itemProvider { let provider = NSItemProvider() provider.suggestedName = fileName provider.registerDataRepresentation(for: .data) { completionHandler in do { // 直接读取文件数据,跳过URL传递 let fileData = try Data(contentsOf: someThrowableOperationToGetTheFileURL()) completionHandler(fileData, nil) } catch { completionHandler(nil, error) } return nil } return provider }
方法3:给Transferable加唯一ID(用Transferable的情况)
如果你用的是SwiftUI的Transferable协议,给自定义的可传输类型加个唯一ID,确保每次生成的实例都是独一无二的,系统就不会缓存失败状态了:
import SwiftUI import UniformTypeIdentifiers struct TransferableFile: Transferable, Identifiable { let id = UUID() // 唯一ID,让系统识别为新资源 let url: URL? let error: Error? static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .data) { item in guard let url = item.url else { throw item.error ?? NSError(domain: "DragError", code: -1, userInfo: [NSLocalizedDescriptionKey: "文件获取失败"]) } return SentTransferredFile(url) } importing: { received in let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(received.file.lastPathComponent) try FileManager.default.copyItem(at: received.file, to: tempURL) return TransferableFile(url: tempURL, error: nil) } } } // 视图里的使用方式 SomeSwiftUIView() .onDrag { do { let fileURL = try someThrowableOperationToGetTheFileURL() return NSItemProvider(object: TransferableFile(url: fileURL, error: nil)) } catch { return NSItemProvider(object: TransferableFile(url: nil, error: error)) } }
小提示
- 方法1适合需要保留原始文件名显示、且文件较大的场景,只是拖拽过程中文件名会带临时后缀,最终用户拿到的文件是正常的。
- 方法2适合小文件,避免大文件占用过多内存。
- 核心思路都是让系统认为每次拖拽的是全新的资源,从而绕过失败状态的缓存。
备注:内容来源于stack exchange,提问作者Yakuhzi
相关产品推荐
相关产品推荐

