0

I emailed an Apple engineer last week about a problem with my NSMetadataQuery.

Here’s the email:

Hi,

I'm writing a document-based app or iOS and my method for renaming (moving the document to a new location) seems to conflict with the running NSMetadataQuery.

The query updates a couple of time after the move method is called, the first time it has the old URL of the item that just moved, and the next it has the new URL. However, because of my updating method (below) if a URL has been removed since the update, my model removes the deleted URL and vice versa for if it finds a URL which doesn't exist yet.

I think my problem is one of two issue, either the NSMetadataQuery's update method is insufficient and doesn't check an item's URL for the 'correct' attributes before deleting it (although looking over documentation I can't see anything that would suggest I'm missing something) or my renaming method isn't doing something it should.

I have tried disabling updates at the start of the renaming method and reenabling once all completion blocks are finished but it doesn't make any difference.

My NSMetadataQuery's update method:

func metadataQueryDidUpdate(notification: NSNotification) {
    ubiquitousItemsQuery?.disableUpdates()

    var ubiquitousItemURLs = [NSURL]()

    if ubiquitousItemsQuery != nil && UbiquityManager.sharedInstance.ubiquityIsAvailable {
        for var i = 0; i < ubiquitousItemsQuery?.resultCount; i++ {
            if let result = ubiquitousItemsQuery?.resultAtIndex(i) as? NSMetadataItem {
                if let itemURLValue = result.valueForAttribute(NSMetadataItemURLKey) as? NSURL {
                    ubiquitousItemURLs.append(itemURLValue)
                }
            }
        }

        //  Remove deleted items
        //
        for (index, fileRepresentation) in enumerate(fileRepresentations) {
            if fileRepresentation.fileURL != nil && !contains(ubiquitousItemURLs, fileRepresentation.fileURL!) {
                removeFileRepresentations([fileRepresentation], fromDisk: false)
            }
        }

        //  Load documents
        //
        for (index, fileURL) in enumerate(ubiquitousItemURLs) {
            loadDocumentAtFileURL(fileURL, completionHandler: nil)
        }

        ubiquitousItemsQuery?.enableUpdates()
    }
}

And my renaming method:

func renameFileRepresentation(fileRepresentation: FileRepresentation, toNewNameWithoutExtension newName: String) {
    if fileRepresentation.name == newName || fileRepresentation.fileURL == nil || newName.isEmpty {
        return
    }

    let newNameWithExtension = newName.stringByAppendingPathExtension(NotableDocumentExtension)!

    //  Update file representation
    //
    fileRepresentation.nameWithExtension = newNameWithExtension

    if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
        self.reloadFileRepresentationsAtIndexPaths([indexPath])
    }

    UbiquityManager.automaticDocumentsDirectoryURLWithCompletionHandler { (documentsDirectoryURL) -> Void in
        let sourceURL = fileRepresentation.fileURL!
        let destinationURL = documentsDirectoryURL.URLByAppendingPathComponent(newNameWithExtension)

        //  Update file representation
        //
        fileRepresentation.fileURL = destinationURL

        if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
            self.reloadFileRepresentationsAtIndexPaths([indexPath])
        }

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
            let coordinator = NSFileCoordinator(filePresenter: nil)
            var coordinatorError: NSError?

            coordinator.coordinateWritingItemAtURL(sourceURL, options: .ForMoving, writingItemAtURL: destinationURL, options: .ForReplacing, error: &coordinatorError, byAccessor: { (newSourceURL, newDestinationURL) -> Void in
                var moveError: NSError?
                let moveSuccess = NSFileManager().moveItemAtURL(newSourceURL, toURL: newDestinationURL, error: &moveError)

                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    assert(moveError == nil || moveSuccess, "Error renaming (moving) document from \(newSourceURL) to \(newDestinationURL).\nSuccess? \(moveSuccess).\nError message: \(moveError).")

                    if let query = self.ubiquitousItemsQuery {
                        query.enableUpdates()
                    }

                    if moveError != nil || moveSuccess {
                        // TODO: Implement resetting file rep
                    }
                })
            })
        })
    }
}

I had a reply almost instantly but since then there’s been no reply.

Here’s the reply

One of the big things that jumps out at me is your usage of disableUpdates() and enableUpdates(). You’re executing them both on the same turn of the run loop, but NSMetadataQuery delivers results asynchronously. Since this code executes within your update notification, it is executing synchronously with respect to the query. So from the query’s point-of-view, it’s going to begin delivering updates by posting the notification. Posting a notification is a synchronous process, so while it’s posting the notification, updates will be disabled and the re-enabled. Thus, by the time the query is done posting the notification, it’s back in the exact same state it was in when it started delivering results. It sounds like that’s not the behavior you’re wanting.

Here’s where I need help

I took this to assume that NSMetadataQuery has some kind of cache which it adds results to while updates are disabled and when enabled, those (perhaps many) cache results are looped through and each are sent via the updates notification.

Anyway, I had a look at run loops on iOS and although I understand them as much as I can on my own, I don’t understand how the reply is helpful, i.e how to actually fix the problem - or what’s even causing the problem.

If anyone has any good idea I’d love your help!

Thanks.

Update

Here’s my log of when functions start and end:

start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
end renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
Adam Carter
  • 4,741
  • 5
  • 42
  • 103

1 Answers1

0

I was having the same problem. NSMetaDataQuery updates tell you if there is a change, but does not tell you what that change was. If the change is a rename, there is no way to identify the previous name, so I can find the old entry in my tableView. Very frustrating.

But, you can get the information by using NSFileCoordinator and NSFilePresenter.

Use the NSFilePresenter method presentedSubitemAtURL(oldURL: NSURL, didMoveToURL newURL: NSURL)

As you noted, the query changed notification is called once with the old URL, and once with the new URL. The method above is called between those two notifications.

David P
  • 35
  • 5