fhe
fhe

Reputation: 6187

Writing NSFilePromiseProviders to pasteboard blocks app on quit

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

Upvotes: 2

Views: 118

Answers (0)

Related Questions