Базовые данные дублируются, когда записи обновляются, но не сохраняются

Я пытаюсь выполнить Задание 6 из курса CS193P Пола Хегарти. Одним словом, это iOS-приложение, используемое для просмотра фотографий, загруженных с Flickr. Приложение имеет две вкладки:

  • Первая вкладка используется для просмотра фотографий по тегу. Когда вы щелкаете по тегу, отображаются фотографии с этим набором тегов. Когда вы нажимаете на фотографию, она отображается. Все это достигается с помощью табличных представлений, встроенных в контроллер навигации, и, наконец, UIScrollView для отображения.
  • Вторая вкладка используется для просмотра недавно просмотренных фотографий в виде таблицы.

Информация о фотографиях и тегах хранится в Core Data. Эти данные отображаются в таблицах через NSFetchedResultsController.

Вот моя проблема: пока я не обновляю объекты Core Data, все в порядке. Когда я обновляю объект (например, устанавливаю свойство lastViewed для фотографии, чтобы отображать ее на вкладке «Недавние»), соответствующая фотография снова загружается с Flickr при следующем обновлении табличного представления, что приводит к дублированию записи в табличное представление. После долгого сеанса отладки я, наконец, обнаружил проблему, но не могу объяснить, почему: это связано с обновлением объекта Core Data без явного сохранения изменений.

Я прочитал Руководство по программированию основных данных, а также различную документацию по классам, но мне не удалось найти ответ по этому поводу.

Вот код для обновления свойства Photo lastViewed, когда пользователь хочет его отобразить. Если я раскомментирую строку [[SharedDocument sharedInstance] saveDocument], все будет работать как положено. Если я его прокомментирую, просмотренное фото будет снова загружено при следующем обновлении, тогда как оно уже существует в Core Data.

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];

    // - Variable check discarded for a readability purpose -

    if ([segue.identifier isEqualToString:@"setImageURL:"])
    {
        Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];

        // Update the last viewed date property to now
        photo.lastViewed = [NSDate dateWithTimeIntervalSinceNow:0];

        // If I uncomment the line below, the issue disappears:
        // [[SharedDocument sharedInstance] saveDocument];

        if ([segue.destinationViewController respondsToSelector:@selector(setImageURL:)])
        {
            // Prepare the next VC
        }
    }
}

NSManagedObjectContext является общим. Вот код из общего объекта:

@interface SharedDocument()
@property (strong, nonatomic)  UIManagedDocument * document;
@end

@implementation SharedDocument

+ (SharedDocument *) sharedInstance
{...} // Returns the shared instance

- (UIManagedDocument *) document
{...} // Lazy instantiation

- (void) useDocumentWithBlock:(void (^)(BOOL success))completionHandler
{...} // Create or open the document

- (void) saveDocument
{
    [self.document saveToURL:self.document.fileURL
            forSaveOperation:UIDocumentSaveForOverwriting
           completionHandler:nil];
}

Обновления:

Примечание о фотографиях Flickr: набор из 50 фотографий доступен для загрузки с Flickr. Это конечный набор, т.е. новые фотографии добавляться и обновляться не будут. Итак, когда я обновляю табличное представление, новая фотография не должна загружаться.

Объект Photo создается следующим образом (это категория из подкласса NSManagedObject):

+ (Photo *) photoWithFlickrInfo:(NSDictionary *)photoDictionary
         inManagedObjectContext:(NSManagedObjectContext *)context
{    
    Photo * photo = nil;

    // Check whether the photo already exists
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"];
    NSString *pred = [NSString stringWithFormat:@"uniqueId = %@",
                      [photoDictionary[FLICKR_PHOTO_ID] description]];
    request.predicate = [NSPredicate predicateWithFormat:pred];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"uniqueId"
                                                                     ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

    NSError *error = nil;

    NSArray *matches = [context executeFetchRequest:request error:&error];

    if (!matches || ([matches count] > 1) || error)
    {
        // Abnormal
        NSLog(@"Error accessing database: %@ (%d matches)", [error description], [matches count]);
    }
    else if (0 == [matches count])
    {
        // Create the photo
        photo = [NSEntityDescription insertNewObjectForEntityForName:@"Photo"
                                              inManagedObjectContext:context];

        photo.uniqueId = photoDictionary[FLICKR_PHOTO_ID];
        photo.title    = [photoDictionary[FLICKR_PHOTO_TITLE] description];
        photo.comment  = [[photoDictionary valueForKeyPath:FLICKR_PHOTO_DESCRIPTION] description];

        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)
        {
            photo.imageURL = [[FlickrFetcher urlForPhoto:photoDictionary
                                                  format:FlickrPhotoFormatOriginal] absoluteString];
        }
        else
        {
            // iPhone
            photo.imageURL = [[FlickrFetcher urlForPhoto:photoDictionary
                                                  format:FlickrPhotoFormatLarge] absoluteString];

        }

        photo.thumbnailURL = [[FlickrFetcher urlForPhoto:photoDictionary
                                                  format:FlickrPhotoFormatSquare] absoluteString];

        photo.section = [photo.title substringToIndex:1];

        // Update the category / tag
        for (NSString * category in [photoDictionary[FLICKR_TAGS] componentsSeparatedByString:@" "])
        {            
            // Ignore a couple of categories
            if ([@[@"cs193pspot", @"portrait", @"landscape"] containsObject:[category lowercaseString]])
                continue;

            Tag *tag = [Tag withName:[category capitalizedString] forPhoto:photo inManagedObjectContext:context];

            [photo addTagsObject:tag];
        }

        NSArray *allTags = [[photo.tags allObjects] sortedArrayUsingComparator:^NSComparisonResult(Tag * t1, Tag * t2) {
            return [t1.name compare:t2.name];
        }];

        photo.tagString = ((Tag *) [allTags objectAtIndex:0]).name;
        NSLog(@"PhotoTagString: %@", photo.tagString);

        // Update the specific 'All' tag
        Tag * allTag = [Tag withName:@"All" forPhoto:photo inManagedObjectContext:context];
        [photo addTagsObject:allTag];

        NSLog(@"[CORE DATA] Photo created: %@ with %d tags", photo.uniqueId, [photo.tags count]);
    }
    else
    {
        // Only one entry
        photo = [matches lastObject];

        NSLog(@"[CORE DATA] Photo accessed: %@", photo.uniqueId);
    }

    return photo;
}

Надеюсь, мое объяснение было достаточно ясным. Скажите, если вам нужно больше информации для понимания вопроса (это мой первый пост, я еще юный падаван :-)

Заранее большое спасибо,

Флориан


person Florian    schedule 05.08.2013    source источник
comment
Как решается, скачивать изображение или нет? Можете показать шляпный код?   -  person Martin R    schedule 05.08.2013
comment
Привет @MartinR. Я загружаю изображение один раз, а затем сохраняю его локально через NSFileManager.   -  person Florian    schedule 05.08.2013


Ответы (1)


Я не вижу из вашего фрагмента кода выше, как вы изначально создаете свой Photo NSManagedObject, но это звучит ужасно, как будто у вас проблема с постоянным идентификатором объекта.

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

Если после вставки вашего управляемого объекта в основные данные вы вызываете что-то вроде этого:

BOOL success = [context obtainPermanentIDsForObjects:@[newEntity] error:&error];

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

В качестве сноски, совершенно не связанной с исходным вопросом, получение идентификаторов по отдельности таким образом очень неэффективно, если вы вставляете пакет объектов, гораздо лучше передать ему массив новых управляемых объектов в этом сценарии. Но это, скорее всего, повлияет только на людей, создающих новый магазин или выполняющих какую-либо другую пакетную вставку.

person Rob Glassey    schedule 05.08.2013
comment
Привет @Роб. При первом запуске приложения метаданные фотографии загружаются с Flickr в виде объекта NSDictionary. Затем для каждой записи словаря создается объект Photo Core Data. Я вставил код создания объекта Photo. - person Florian; 05.08.2013
comment
Спасибо за это. Я не вижу каких-либо явных ошибок в недавно добавленном коде, поэтому ответ, который я дал выше, остается в силе - после создания нового объекта фотографии заставьте контекст получить его постоянный идентификатор. Это всего лишь интуиция, так что я могу уйти. - person Rob Glassey; 05.08.2013
comment
Единственное, что немного не так, это то, что в вашем предикате вы используете [photoDictionary[FLICKR_PHOTO_ID] description], тогда как при создании фотографии вы устанавливаете photo.uniqueId = photoDictionary[FLICKR_PHOTO_ID]. Лучше всего оставить их одинаковыми, а при создании предиката использовать predicateWithFormat за один шаг — не создавайте сначала строку, сделайте это в одной строке, например request.predicate = [NSPredicate predicateWithFormat:@"uniqueId = %@",photoDictionary[FLICKR_PHOTO_ID]] - person Rob Glassey; 05.08.2013
comment
Большое спасибо @Rob, это действительно очень хороший улов. Установка предиката на одну строку вместо двух сделала свое дело (я несколько раз тестировал одну строку, а затем две строки, чтобы убедиться, что это действительно проблема). Однако я не понимаю, почему. У вас есть какая-нибудь подсказка по этому поводу? - person Florian; 05.08.2013
comment
На самом деле разница есть, как объясняется здесь. - person Florian; 05.08.2013
comment
Ударь меня! Кроме того, если вы использовали description, вы не обязательно сравнивали подобное с подобным - это не должно было вызвать проблемы, если базовый объект был строкой, но описание обычно используется для получения отладочной печатной версии объекта, и его можно переопределить, поэтому не всегда будет получаться то же самое. Это ленивый способ преобразования объекта в строку, игнорирующий проблемы локализации и т. д. Лучше использовать средство форматирования, и тогда вы избежите возможности неожиданного изменения функциональности description в будущем. - person Rob Glassey; 05.08.2013