20

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:

  1. The download for the file starts without error
  2. The file attributes for the file's download status always return that it's not downloading, has not finished downloading, and the progress is 0 percent.
  3. I'm stuck in the loop forever, even though the file size changes in between checks.

What am I doing wrong?

Craig Otis
  • 31,257
  • 32
  • 136
  • 234

4 Answers4

13

Years later I am still experiencing periodic (though much more rare after some of the workarounds in other answers) issues downloading files. So, I contacted the developers at Apple asking for a technical review/discussion and here's what I found.

Periodically checking for the download status of the same NSURL, even if you're recreating it, is not the preferred method for checking the status. I don't know why it isn't - it seems like it should work, but it doesn't. Instead, once you begin to download the file, you should register an observer with NSNotificationCenter for the progress of that download, maintaining a reference to the query for that file. Here's the exact code sample that was provided to me. I've implemented it (with some app-specific tweaks) in my app, and it seems to be performing much more appropriately.

- (void)download:(NSURL *)url
{
    dispatch_queue_t q_default;
    q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(q_default, ^{

        NSError *error = nil;
        BOOL success = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:url error:&error];
        if (!success)
        {
            // failed to download
        }
        else
        {
            NSDictionary *attrs = [url resourceValuesForKeys:@[NSURLUbiquitousItemIsDownloadedKey] error:&error];
            if (attrs != nil)
            {
                if ([[attrs objectForKey:NSURLUbiquitousItemIsDownloadedKey] boolValue])
                {
                    // already downloaded
                }
                else
                {
                    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
                    [query setPredicate:[NSPredicate predicateWithFormat:@"%K > 0", NSMetadataUbiquitousItemPercentDownloadedKey]];
                    [query setSearchScopes:@[url]]; // scope the search only on this item

                    [query setValueListAttributes:@[NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemIsDownloadedKey]];

                    _fileDownloadMonitorQuery = query;

                    [[NSNotificationCenter defaultCenter] addObserver:self
                                                             selector:@selector(liveUpdate:)
                                                                 name:NSMetadataQueryDidUpdateNotification
                                                               object:query];

                    [self.fileDownloadMonitorQuery startQuery];
                }
            }
        }
    });
}

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

    if (query != self.fileDownloadMonitorQuery)
        return; // it's not our query

    if ([self.fileDownloadMonitorQuery resultCount] == 0)
        return; // no items found

    NSMetadataItem *item = [self.fileDownloadMonitorQuery resultAtIndex:0];
    double progress = [[item valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey] doubleValue];
    NSLog(@"download progress = %f", progress);

    // report download progress somehow..

    if ([[item valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey] boolValue])
    {
        // finished downloading, stop the query
        [query stopQuery];
        _fileDownloadMonitorQuery = nil;
    }
}
Craig Otis
  • 31,257
  • 32
  • 136
  • 234
  • it is not working for me when scope is defined as url. – Marcin Nov 04 '14 at 01:03
  • This is currently not working for me (8.1.2), either setting the scope as an URL or `NSMetadataQueryUbiquitousDocumentsScope`, and changing from the deprecated `NSMetadataUbiquitousItemIsDownloadedKey` to `NSMetadataUbiquitousItemDownloadingStatusKey` the `liveUpdate:` callback never happens. – voidref Dec 18 '14 at 04:28
  • I might also note that `resourceValuesForKeys` on a valid UbiquityContainer URL always returns an empty array for me. – voidref Dec 18 '14 at 04:30
  • @voidref I would recommend asking as a separate question. If you think it's a bug, you can contact DTS @ Apple and they'll be able to sort it out. – Craig Otis Dec 21 '14 at 17:41
  • Thanks @CraigOtis, I filed a related radar with information specific to my circumstance. In the meantime, I guess my users will just have to enjoy an indefinite spinner! =) – voidref Dec 23 '14 at 23:46
  • 1
    You forgot to remove observer. – pronebird Dec 17 '15 at 18:22
11

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.

(Additional reference)

Community
  • 1
  • 1
MrGomez
  • 23,788
  • 45
  • 72
  • Thanks for the links - very unfortunate that Apple is content with leaving things in such a sorry state. – Craig Otis Mar 26 '12 at 11:25
  • @MrGomez ... Apparently I am having the same situation here. Where should I put the delay? That part wasn't very clear to me :( – nacho4d May 14 '12 at 10:35
  • @nacho4d I would recommend looking at the [code package](http://www.freelancemadscience.com/files/MultiDocument.zip) (ZIP file) linked by the aforementioned article on the issue. Tersely speaking, you need to add a delay between the transfer of each individual file to my understanding. If you require additional assistance, I recommend posting a follow-up question with your specific code given. Best of luck to you! :) – MrGomez May 14 '12 at 18:43
  • Is there a new solution for this problem? I have run into this problem, when using trying to load files from iCloud. I added `[NSThread sleepForTimeInterval:(rand() % 10)];` before the `if([file getResourceValue...` statements. It helped a little, but is there a better place to put the delay? – Joseph Nov 11 '12 at 10:36
  • @Casper Unfortunately, I haven't kept this issue under constant monitor. My advice would be to start a new question if the advice here and in other questions does not solve your problem, linking back to this thread as something you've tried. I wish I had more time nowadays to look into this... but I'm sure someone else does, and that they'll be happy to answer your question. :) – MrGomez Nov 13 '12 at 18:32
5

Encountered this same situation myself recently -- just as above, I saw the download clearly happening, but NSURLUbiquitousItemIsDownloading, and all other keys, always returned false. A colleague mentioned that an iCloud engineer had recommended creating a new NSURL to check these NSURLUbiquitousItem keys since the metadata may not (and apparently will not) update after it's created. I created a new NSURL prior to checking, and it indeed reflected the current state.

Not to discount the race conditions mentioned by @MrGomez, which are significant problems (especially prevalent in iOS 5, and caused us many headaches), but I don't believe explain the issue described above.

EDIT: In order to create the new NSURL I used [NSURL fileURLWithPath:originalURL.path]. While a bit messy, it was the first thing the worked reliably. I just tried [originalURL copy] and ended up with old metadata again, so apparently it too was copied.

For safety, or until its documented further, I plan on assuming that unless a new NSURL is created prior to any getResourceValue:forKey: call, stale metadata will be returned.

gyratory circus
  • 437
  • 6
  • 15
  • So when provided an `NSURL` object to examine, how do you create the new `NSURL` to check the updated state? Do you call `copy` on the old one, or use some other constructor, with values obtained from the original? And I assume you re-create this new `NSURL` at the beginning of each of your download 'passes'? – Craig Otis Aug 28 '13 at 11:04
0

I'm facing the same problem and I've spent many days trying to find a workaround for this bug...

Unfortunately, none of the solutions here work for me and I see that the document I want to open is always up to date after 2 attempts for opening it.

So the solution for me is to open the document twice this way :

  • Make a blind opening first ;
  • Make a blind closing ;
  • Waiting for the downloaded state ;
  • Then open the document.

I'm aware that this is a very dirty way to do the job but it work fine for me :-)

Petter Friberg
  • 21,252
  • 9
  • 60
  • 109