Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I determine if file exists in iCloud folder?

I have an iOS app that stores files in iCloud. When I start up the app I want to determine if a previous device has already uploaded any files. I start the first device and it adds the files to iCloud (I can see them in the Mobile Documents folder on my Mac). I then start the app on a second device and try to use the following NSMetadataQuery to see if any files have been uploaded, but it returns 0 results. If I keep running that query, after about 8-10 seconds it does return results.

iCloudQuery = [[NSMetadataQuery alloc] init];

iCloudQuery.searchScopes = @[NSMetadataQueryUbiquitousDataScope];

NSString *filePattern = [NSString stringWithFormat:@"*.%@", @"txt"];

iCloudQuery.predicate = [NSPredicate predicateWithFormat:@"%K LIKE %@", NSMetadataItemFSNameKey, filePattern];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iCloudQueryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:iCloudQuery];

[iCloudQuery startQuery];

When I get the notification resultCount is 0 for the query

- (void)iCloudQueryDidFinishGathering:(NSNotification *)notification
{    
    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    NSLog(@"Found %d results from metadata query", query.resultCount);
}

Shouldn't NSMetadataQuery return a resultCount if the file exists in iCloud, even if it hasn't been downloaded? Is there any way to test if a file exists other than trying over an over and timing out after 15-30 seconds?

like image 434
Austin Avatar asked Jul 06 '13 03:07

Austin


1 Answers

It might take some time for the query to retrieve the metadata from iCloud. The didFinishGathering may initially only hold the results that the device already knows about, not changes it hasn't had a chance to hear about from iCloud.

Rather than stopping and starting your NSMetadataQuery, it would be preferable to set one up and keep listening to it by also registering for:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(iCloudQueryDidUpdate:)
                                             name:NSMetadataQueryDidUpdateNotification
                                           object:iCloudQuery];

... and retrieve the updates when they come in. So you'll need to change your finishGathering method as well, not to stop the query, and to enableUpdates at the end.

You'll have to rethink your approach somewhat to allow for the fact that the first set of results won't necessarily know everything yet. More normally, NSMetadataQuery is used to keep watch on iCloud, with the expectation that changes generated by other devices can come along at any time - not just on app launch.

If you need to be sure you have the most up-to-date metadata for iCloud the only approach I've found reliable (on both iOS 5 and iOS 6) is to inject a small file into iCloud (usually with a distinct form of name, and named with a UUID so it is guaranteed to be unique), and then in the iCloudQueryDidUpdate: method, not considering the query results to be complete until that file is both returned by the query, and it's metadata is reporting that it is also uploaded to iCloud. Once you get this back, you can be fairly certain you have received the latest metadata from iCloud.

Check for upload in iCloudQueryDidUpdate: using:

int resultCount = [iCloudQuery resultCount];

for (int i = 0; i < resultCount; i++) {
  NSMetadataItem *item = [iCloudQuery resultAtIndex:i];

  BOOL isUploaded = [[item valueForAttribute:NSMetadataUbiquitousItemIsUploadedKey] boolValue];
  BOOL isDownloaded = [[item valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey] boolValue];
  NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
  BOOL documentExists = [[NSFileManager defaultManager] fileExistsAtPath:[url path]];

  // You'll need to check isUploaded against the URL of the file you injected, rather than against any other files your query returns
}

And don't forget to delete the injected file when you are finished with it - otherwise these will mount up each time your app launches.

Edit:

The way I'd implemented these checks had an in-built delay, that once I took it out I found a case where the above wasn't entirely reliable.

I had deleted metadata items (deleted using Settings/iCloud/Storage & Backup/Manage Storage before the current run) being reported as uploaded and downloaded and existing on disk before the full metadata came back for my injected file. However, once the injected file was reported as uploaded, downloaded, and existing locally on disk, one of these deleted files was still listed in the metadata as uploaded and downloaded - BUT NOT existing on disk.

So what it looks like has been happening is the iCloud daemon hears about the pending deletion on the old data, and actually carries out the deletion BEFORE the metadata your app sees has been updated to reflect this. Crazy, huh? So, I have to update my recommendation above, to only consider query results complete if the item is reported as downloaded, uploaded AND it exists in the local folder by using the [NSFileManager fileExistsAtPath:] method. Code above edited to reflect this.

After this, all you can do is stick something like a 1 second delay in before acting on the results of the query, to be absolutely sure all metadata has had time to be received - though this is something I HATE having to do. Sticking spurious time delays into code to make it work feels a bit too close to black magic to me. And indicative that you don't really understand what is going on - though without more hooks into the processing behind iCloud, what else are we to do?

like image 198
Rob Glassey Avatar answered Sep 29 '22 09:09

Rob Glassey