Способ загрузки файлов iCloud? Очень смущает?

У меня есть базовая поддержка iCloud в моем приложении (синхронизация изменений, создание вездесущих и т.д.), но одним из основных недостатков до сих пор было отсутствие поддержки загрузки файлов, которые существуют (или имеют изменения) в облаке, но не синхронизируются с тем, что в настоящее время находится на диске.

Я добавил следующие методы в мое приложение на основе некоторого кода, предоставленного Apple, с помощью нескольких настроек:

Методы загрузки:

- (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]];
        }
    }
}

И код, который запускает/проверяет загрузки:

 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];
        }

Насколько я могу судить, приведенный выше код кажется прекрасным, но когда я делаю большое количество изменений на устройстве A, сохраните эти изменения, а затем откройте устройство B (чтобы запросить загрузку на устройстве B) это то, что я вижу в консоли:

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

Так по какой-то причине:

  • Загрузка файла начинается без ошибок
  • Атрибуты файла для состояния загрузки файла всегда возвращают, что он не загружается, не загружается, а прогресс составляет 0 процентов.
  • Я застрял в цикле навсегда, даже если размер файла изменяется между проверками.

Что я делаю неправильно?

Ответ 1

Через несколько лет я все еще испытываю периодические (хотя и гораздо реже после некоторых обходных путей в других ответах) проблемы с загрузкой файлов. Итак, я связался с разработчиками Apple, предлагая техническую проверку/обсуждение, и вот что я нашел.

Периодически проверка статуса загрузки того же NSURL, даже если вы его воссоздаете, не является предпочтительным методом проверки состояния. Я не знаю, почему это не так - похоже, он должен работать, но это не так. Вместо этого, как только вы начнете загружать файл, вы должны зарегистрировать наблюдателя с NSNotificationCenter для выполнения этой загрузки, поддерживая ссылку на запрос для этого файла. Вот пример кода, который мне был предоставлен. Я реализовал его (с некоторыми настройками для приложения) в своем приложении, и, похоже, он выполняет гораздо более подходящий подход.

- (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 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;
    }
}

Ответ 2

Это известная проблема. Она была воспроизведена здесь и .

Кажется, добавление задержки помогает смягчить неизвестное состояние гонки, но пока не существует известного способа обхода проблемы. С 21 марта:

В любом случае, мне было интересно, если вы когда-нибудь преодолели основную проблему в этом статья? То есть со временем синхронизация, похоже, ухудшается, и импорт уведомления перестают поступать или являются неполными. Вы определили, что добавление задержки в ответе на уведомление об импорте но в конечном итоге оказался ненадежным.

И из OP связанной статьи:

Проблема, которая, как мне казалось, была вызвана состоянием гонки. Иногда я получит уведомление о том, что мой постоянный магазин был обновлен от iCloud - но обновленная информация пока недоступна. Кажется, это произошло примерно через 1/4 времени без задержки, и около 1/12-й раз с задержкой.

Это не было похоже на ухудшение стабильности... система всегда будет ловить обновление при следующем запуске приложения и автоконфликт разрешение проблемы. И тогда он будет продолжать функционировать как обычно. Но, в конце концов, он потеряет еще одно обновление.

...

На каком-то уровне я думаю, нам просто нужно верить, что iCloud будет в конечном итоге вытолкнуть информацию, и наш код разрешения конфликтов (или автоматическое разрешение конфликтов данных ядра) устранит любые проблемы которые возникают.

Тем не менее, я надеюсь, что Apple исправит ошибку гонки. Это просто не должно произойдет.

Итак, на момент написания, по крайней мере, загрузка iCloud с использованием этого API должна рассматриваться как ненадежная.

(Дополнительная ссылка)

Ответ 3

В последнее время я столкнулся с такой же ситуацией - так же, как и выше, я видел, что загрузка явно происходит, но NSURLUbiquitousItemIsDownloading и все остальные ключи всегда возвращались false. Коллега упоминал, что инженер iCloud рекомендовал создать новый NSURL для проверки этих NSURLUbiquitousItem ключей, поскольку метаданные могут (и, по-видимому, не будут) обновляться после его создания. Я создал новый NSURL перед проверкой, и он действительно отразил текущее состояние.

Не обесценивать условия гонки, упомянутые @MrGomez, которые являются значительными проблемами (особенно распространенными в iOS 5, и вызвали у нас много головных болей), но я не верю, объясняю проблему, описанную выше.

EDIT: Чтобы создать новый NSURL, я использовал [NSURL fileURLWithPath:originalURL.path]. В то время как немного беспорядочно, это было первое, что работало надежно. Я просто попробовал [originalURL copy] и снова получил старые метаданные, поэтому, видимо, он тоже был скопирован.

В целях безопасности или до его дальнейшего документирования я предполагаю, что если новый NSURL не будет создан до вызова getResourceValue:forKey:, будут возвращены устаревшие метаданные.

Ответ 4

Я столкнулся с той же проблемой, и я потратил много дней, пытаясь найти обходной путь для этой ошибки...

К сожалению, ни одно из решений здесь не работает для меня, и я вижу, что документ, который я хочу открыть, всегда обновляется после двух попыток его открытия.

Итак, решение для меня состоит в том, чтобы открыть документ дважды:

  • Сначала сделайте слепое открытие;
  • Сделать закрытие закрытия;
  • Ожидание загруженного состояния;
  • Затем откройте документ.

Я знаю, что это очень грязный способ выполнить эту работу, но он отлично работает для меня: -)