Writing NSFilePromiseProviders to pasteboard blocks app on quit

55 views Asked by At

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 NSFilePromiseProviders 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

0

There are 0 answers