문제

I'm trying to complete the Assignment 6 from course CS193P of Paul Hegarty. In a word, this is an iOS application used to browse photos downloaded from Flickr. The application has two tabs:

  • The first tab is used to browse photos by tag. When you click a tag, this lists the photos with this tag set. When you click the photo, it is displayed. All this is achieved through Table Views embedded in a Navigation Controller, and finally a UIScrollView for the display.
  • The second tab is used to browse recently viewed photos through a Table View.

Photo and tag information is stored in Core Data. This data is displayed in tables through a NSFetchedResultsController.

Here is my issue: as long as I do not update the Core Data objects, everything is fine. When I update an object (for example, setting the lastViewed property on a photo, so that I can display it in the Recent tab), the corresponding photo is downloaded again from Flickr on next Table View Refresh, which leads to a duplicate entry in the Table View. After a long debugging session, I finally discovered the issue, but I cannot explain why: this is due to an update on a Core Data object without explicitly saving the change.

I've read the Core Data Programming Guide, as well as the different Class Reference documentations, but I didn't manage to find an answer regarding this.

Here is the code to update the Photo lastViewed property when the user wants to display it. If I uncomment the line [[SharedDocument sharedInstance] saveDocument], everything works as expected. If I comment it, the viewed photo will be downloaded again on next refresh, whereas it already exists in 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
        }
    }
}

The NSManagedObjectContext is shared. Here is the code from the shared object:

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

Updates:

Note about Flickr photos: a set 50 photos is available for download from Flickr. This is a finite set, i.e. no new photos will be added nor updated. So, when I refresh a Table View, no new Photo should be downloaded.

A Photo object is created this way (this is a Category from a NSManagedObject subclass):

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

I hope my explanation was clear enough. Tell me if you need more information to understand the issue (this is my first post, I'm still a young padawan :-)

Thanks a lot in advance,

Florian

도움이 되었습니까?

해결책

I can't see from your code snippet above how you initially create your Photo NSManagedObject, but it sounds awfully like you are having a permanent object ID issue.

When using UIManagedDocument, there's an issue with this not being done automatically for you on save, the problem usually manifests as fetching of newly created objects failing until the next app launch. This is because it is running off the initial temporary object ID even after you save (normally on save you would expect the permanent object IDs to be created and assigned for you). This I guess is why it works up until you save, but not after.

If after inserting your managed object into core data you call something like this:

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

It will create the permanent ID, you can save the document as normal, and any fetches should find the new object.

As a footnote, and entirely unrelated to the original question, obtaining IDs individually in this manner is very inefficient if you are inserting a batch of objects, much better to pass it an array of new managed objects in this scenario. But this is likely to only affect people seeding a new store, or doing some other batch insert.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top