Reputation: 6187
In my Mac app, I'm writing NSFilePromiseProvider
objects (initialized with fileType kUTTypeFileURL
) to the general pasteboard on Edit → Copy. This way, the files can be copied to other apps, e.g., Finder. However, when the app is quit there is a huge delay (including beachballing) before actually closing the app and the Console log shows that pasteboard activity occurs. It looks like the system is in a way ensuring that the promises can still be provided.
All of the examples I found are using NSFilePromiseProvider
s in the context of drag and drop from table views and I found nothing that writes the objects directly to the pasteboard (although it implements NSPasteboardWriting
).
Through lots of experimentation, I've now discovered that if my own version of NSFilePromiseProvider
returns []
at writingOptions(forType:pasteboard:)
for all types, closing the app does not show the unwanted delay.
However, as I don't understand 1.) why the issue occurs in the first place, and 2. why the workaround circumvents this problem, I wanted to ask here to potentially gain some insights.
Here is the implementation of the NSFilePromiseProvider
in question:
class FilePromiseProvider: NSFilePromiseProvider {
struct UserInfoKeys {
static let fileURL = "fileURLKey"
static let row = "rowKey"
}
override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
var types = super.writableTypes(for: pasteboard)
if let userInfoDict = userInfo as? [String: Any] {
if userInfoDict[UserInfoKeys.row] != nil {
types.append(.fileListTableRow)
}
if userInfoDict[UserInfoKeys.fileURL] != nil {
types.append(.fileURL)
types.append(.string)
}
}
return types
}
override func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.WritingOptions {
// If we return [] here, the app closes without delay and extra work by the system.
// It seems that returning `.promise` for some of the types (which is done by the
// default implementation) flags some of the files to be made available to a
// system-internal location, causing the app to block for a while when closing.
super.writingOptions(forType: type, pasteboard: pasteboard)
}
override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
guard let userInfoDict = userInfo as? [String: Any] else { return nil }
switch type {
case .fileListTableRow:
if let row = userInfoDict[UserInfoKeys.row] as? Int {
return row
}
case .fileURL:
if let url = userInfoDict[UserInfoKeys.fileURL] as? NSURL {
return url.pasteboardPropertyList(forType: type)
}
case .string:
if let url = userInfoDict[UserInfoKeys.fileURL] as? NSURL {
return url.lastPathComponent
}
default: break
}
return super.pasteboardPropertyList(forType: type)
}
}
The copy
action writes the promises to the pasteboard:
@IBAction func copy(_ sender: Any?) {
guard let urls = representedObject as? [URL] else { return }
let itemIndexes = tableView.selectedRowIndexes
if itemIndexes.isEmpty {
return
}
var filePromises = [FilePromiseProvider]()
for idx in itemIndexes {
var userInfo = [String: Any]()
userInfo[FilePromiseProvider.UserInfoKeys.fileURL] = urls[idx]
let filePromise = FilePromiseProvider(fileType: UTType.fileURL.identifier, delegate: self)
filePromise.userInfo = userInfo
filePromises.append(filePromise)
}
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects(filePromises)
}
and the respective implementation of NSFilePromiseProviderDelegate
provides the files and file URLs.
extension ViewController: NSFilePromiseProviderDelegate {
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let fileURL = queryFileURL(from: filePromiseProvider)
return fileURL?.lastPathComponent ?? ""
}
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) {
do {
if let atURL = queryFileURL(from: filePromiseProvider) {
try FileManager.default.copyItem(at: atURL, to: url)
}
completionHandler(nil)
} catch let error {
OperationQueue.main.addOperation {
self.presentError(error, modalFor: self.view.window!, delegate: nil, didPresent: nil, contextInfo: nil)
}
completionHandler(error)
}
}
func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return filePromiseQueue
}
private func queryFileURL(from filePromiseProvider: NSFilePromiseProvider) -> URL? {
if let userInfo = filePromiseProvider.userInfo as? [String: Any] {
return userInfo[FilePromiseProvider.UserInfoKeys.fileURL] as? URL
}
return nil
}
}
I've added a complete project for easy testing on my Github:
https://github.com/fheidenreich/file-promise-copy
Upvotes: 2
Views: 118