4

I have two entities one called Post and one called User. Post<<---->User is the relationship in core data. I am using a NSFetchedResultsController to fetch all Post records in my core data stack and then displaying them in a UITableView. Each cell has an image and that image corresponds to a User.profilePicture.

Upon initializing I do not download the profile picture from the server, I only download when it scrolls past that cell (lazy load). Once I download it I save the downloaded image to the corresponding User.profilePicture in the core data stack.

Is there a way for controllerDidChangeContent to be called when I update the User entity?? My current understanding is that my NSFetchedResultsController can only follow the Post entity since that is what I initially set it to do and cannot traverse and monitor updates across a relationship, is that true?

inks2002
  • 377
  • 4
  • 13

2 Answers2

4

Sadly I know only of an UGLY solution for this issue.

In your User .m file implements the setProfilePicture: like this:

//NOT TESTED IN A MULTITHREADED ENV
- (void) setProfilePicture:(NSData *)data
{
    [self willChangeValueForKey:@"profilePicture"];
    [self setPrimitiveValue:data forKey:@"profilePicture"];
    [self.posts enumerateObjectsUsingBlock:^(Post* p, BOOL *stop) {
        [p willChangeValueForKey:@"user"];
        [p didChangeValueForKey:@"user"];
    }];
    [self didChangeValueForKey:@"profilePicture"];
}

This will notify the FRC that the Post element has changes.

You might find additional information here

Edit:

To fetch the data on access you can add this to your User.m:

//UNTESTED
+ (void) mergeToMain:(NSNotification*)notification
{
    AppDelegate* appDel = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [appDel.managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) 
                                                  withObject:notification 
                                               waitUntilDone:YES];
}

- (NSData*)_profilePicture
{
    return [self primitiveValueForKey:@"profilePicture"];
}

- (NSData*) profilePicture
{
    [self willAccessValueForKey:@"profilePicture"];
    NSData* picData = [self primitiveValueForKey:@"profilePicture"];
    if (!name) {
        __block NSManagedObjectID* objectID = self.objectID;
        //This solves the multiple downloads per item by using a single queue
        //for all profile pictures download.
        //There are more concurrent ways to accomplish that
        dispatch_async(downloadSerialQueue, ^{ //define some serial queue for assuring you down download multiple times the same object
            NSError* error = nil;
            AppDelegate* appDel = (AppDelegate*)[[UIApplication sharedApplication] delegate];
            NSManagedObjectContext* context = [[NSManagedObjectContext alloc] init];
            [context setPersistentStoreCoordinator:appDel.persistentStoreCoordinator];
            [context setUndoManager:nil];
            User* user = (User*)[context existingObjectWithID:objectID error:&error];
            if (user && [user _profilePicture] == nil) {
                NSData *data = //[method to retrieve data from server];
                if (data) {
                    if (user) {
                        user.profilePicture = data;
                    } else {
                        NSLog(@"ERROR:: error fetching user: %@",error);
                        return;
                    }
                    [[NSNotificationCenter defaultCenter] addObserver:[self class] selector:@selector(mergeToMain:) name:NSManagedObjectContextDidSaveNotification object:context];
                    [context save:&error];
                    [[NSNotificationCenter defaultCenter] removeObserver:[self class] name:NSManagedObjectContextDidSaveNotification object:context];
                }                    
            }
        });
    }
    [self didAccessValueForKey:@"profilePicture"];
    return picData;
}
Dan Shelly
  • 5,991
  • 2
  • 22
  • 26
  • hmm I will have to try this, it doesn't seem that ugly...in a multithreaded env would those KVO messages need to be on the main thread? – inks2002 Apr 26 '13 at 19:06
  • I believe that it would work even then, but have not tested it yet (will do so later) – Dan Shelly Apr 26 '13 at 19:08
  • Tested, and it update when merging changes from other threads. – Dan Shelly Apr 26 '13 at 19:18
  • I will test for my implementation in the next day when I have access to Xcode and update on the status. Thanks for your help. One question, I was planning on implementing this gist... https://gist.github.com/jkemink/45458176c7a9a5f7213d ....Basically I call the property, if it hasn't been downloaded I return nil/blank value, download it, set it, kvo kicks off and then updates my tableview. does my gist in a way make sense, or is it VERY UGLY doing it that way. – inks2002 Apr 26 '13 at 20:01
  • This is very risky. if you deallocate the context in mid-download and don't save properly, you will loose the data. I would suggest performing a fetch with an `objectID` and import and set the data on success and would maintain the currently fetched `objectID` in a central location to avoid unnecessary/duplicate downloads. – Dan Shelly Apr 26 '13 at 20:33
  • My context is managed through a Core Data Helper class, I don't really know when I would deallocate it? The Gist code just basically sets the property, it doesn't write it back I assume. I periodically save un-saved data so I'm not overly concerned if the profile picture is lost, re-downloading is not a worst case in my opinion. But your idea would be something that could definitely help if I need to maintain better consistency. My main concern for the gist code is that if it violates good programming practices, which it doesn't seem to to me at least. – inks2002 Apr 26 '13 at 20:44
  • You could keep a strong reference to the context importing the item and make sure it is not allocated (pass it as a strong property in your view controller) – Dan Shelly Apr 26 '13 at 21:15
  • I will try the edit out thanks!! One thing though, I don't think that will trigger an update on my Post entity NSFetchedResultsController (call to controllerDidChangeContent), or will it? I might have to merge my code (Gist) with yours above, I will update later. – inks2002 Apr 26 '13 at 23:23
  • Thank you @Dan Shelly for your help, I went with a combination of your edit and your first post. I did not implement saving in the User object as if I loose the profile picture I'm not concerned as it is a minor fetch. But I will be using your code for larger downloads for sure. See the gist... https://gist.github.com/jkemink/c855bda91f206d43e74f – inks2002 Apr 27 '13 at 17:18
-2

I think this issue can be solved without NSFetchedResultsController involved.

  1. use SDWebImage, SDWebImage can load images from remote server asynchronously, just do this:

    [myImageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                   placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
    
  2. use KVO, add a observer to User entity and update corresponding image view accordingly. But the code for KVO is rather complex, ReactiveCocoa can simplify them:

    [RACAble(user.profilePicture) subscribeNext:^(UIImage *image) {
        [myImageView setImage:image];
    }];
    
leafduo
  • 144
  • 1
  • 5