I have basic iCloud support in my application (syncing changes, making ubiquitous, etc.), but one crucial omission so far has been the lack of "download" support for files that exist (or have changes) in the cloud, but are not in sync with what's currently on disk.
I added the following methods to my application, based on some Apple-provided code, with a couple tweaks:
The download methods:
- (BOOL)downloadFileIfNotAvailable:(NSURL*)file {
NSNumber* isIniCloud = nil;
if ([file getResourceValue:&isIniCloud forKey:NSURLIsUbiquitousItemKey error:nil]) {
// If the item is in iCloud, see if it is downloaded.
if ([isIniCloud boolValue]) {
NSNumber* isDownloaded = nil;
if ([file getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]) {
if ([isDownloaded boolValue])
return YES;
// Download the file.
NSFileManager* fm = [NSFileManager defaultManager];
NSError *downloadError = nil;
[fm startDownloadingUbiquitousItemAtURL:file error:&downloadError];
if (downloadError) {
NSLog(@"Error occurred starting download: %@", downloadError);
}
return NO;
}
}
}
// Return YES as long as an explicit download was not started.
return YES;
}
- (void)waitForDownloadThenLoad:(NSURL *)file {
NSLog(@"Waiting for file to download...");
id<ApplicationDelegate> appDelegate = [DataLoader applicationDelegate];
while (true) {
NSDictionary *fileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:[file path] error:nil];
NSNumber *size = [fileAttribs objectForKey:NSFileSize];
[NSThread sleepForTimeInterval:0.1];
NSNumber* isDownloading = nil;
if ([file getResourceValue:&isDownloading forKey:NSURLUbiquitousItemIsDownloadingKey error:nil]) {
NSLog(@"iCloud download is moving: %d, size is %@", [isDownloading boolValue], size);
}
NSNumber* isDownloaded = nil;
if ([file getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]) {
NSLog(@"iCloud download has finished: %d", [isDownloaded boolValue]);
if ([isDownloaded boolValue]) {
[self dispatchLoadToAppDelegate:file];
return;
}
}
NSNumber *downloadPercentage = nil;
if ([file getResourceValue:&downloadPercentage forKey:NSURLUbiquitousItemPercentDownloadedKey error:nil]) {
double percentage = [downloadPercentage doubleValue];
NSLog(@"Download percentage is %f", percentage);
[appDelegate updateLoadingStatusString:[NSString stringWithFormat:@"Downloading from iCloud (%2.2f%%)", percentage]];
}
}
}
And the code that starts/checks the downloads:
if ([self downloadFileIfNotAvailable:urlToUse]) {
// The file is already available. Load.
[self dispatchLoadToAppDelegate:[urlToUse autorelease]];
} else {
// The file is downloading. Wait for it.
[self performSelector:@selector(waitForDownloadThenLoad:) withObject:[urlToUse autorelease] afterDelay:0];
}
As far as I can tell the above code seems fine, but when I make a large number of changes on Device A, save those changes, then open Device B (to prompt a download on Device B) this is what I see in the console:
2012-03-18 12:45:55.858 MyApp[12363:707] Waiting for file to download...
2012-03-18 12:45:58.041 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.041 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.041 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.143 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.143 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.144 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.246 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.246 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.246 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.347 MyApp[12363:707] iCloud download is moving: 0, size is 177127
2012-03-18 12:45:58.347 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.347 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.449 MyApp[12363:707] iCloud download is moving: 0, size is 177127
2012-03-18 12:45:58.449 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.450 MyApp[12363:707] Download percentage is 0.000000
So for whatever reason:
What am I doing wrong?
This is a known issue. It's been reproduced here and here.
It seems adding a delay helps alleviate an unknown race condition, but no known workaround yet exists. From March 21st:
Anyway, I was wondering if you ever got past your main issue in this article? That is, over time, syncing seems to degrade and the import notifications stop arriving or are incomplete. You had identified that adding a delay in responding to the import notification had some value, but eventually proved to be unreliable.
And from the OP of the linked article:
The problem I had seemed to be caused by a race condition. Sometimes I would get the notification that my persistent store had been updated from iCloud--but the updated information would not be available yet. This seemed to happen about 1/4 of the time without the delay, and about 1/12th of the time with the delay.
It wasn't like the stability degraded...the system would always catch the update the next time I launched the app, and the auto conflict resolution fixed the problem. And then it would continue to function normally. But, eventually, it would drop another update.
...
At some level, I think we just need to trust that iCloud will eventually push out the information, and our conflict resolution code (or core data's automatic conflict resolution) will fix any problems that arise.
Still, I hope Apple fixes the race condition bug. That just shouldn't happen.
So, at the time of this writing at least, iCloud downloads using this API should be treated as unreliable.