Asyc image download with core data - ios

I'm wondering how to do this in proper way.
I have NSManagedObject which contains properties: name, imageUrl, iconUrl.
I'm using category to update this object:
#implementation MyObject (Create)
+ (instancetype)findOrCreateWithIdentifier:(id)identifier inContext:(NSManagedObjectContext*)context {
NSString* entityName = NSStringFromClass(self);
NSFetchRequest* fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name = %#", identifier];
fetchRequest.fetchLimit = 1;
id object = [[context executeFetchRequest:fetchRequest error:NULL] lastObject];
if(object == nil) {
object = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
}
return object;
}
+ (void)createWithJSONComponents:(NSDictionary*)components intoContext:(NSManagedObjectContext*)context
{
NSString* name = components[#"name"];
NSString* imageUrl = components[#"image"];
NSString* iconUrl = components[#"icon"];
MyObject* myObject = [self findOrCreateWithIdentifier:myObject inContext:context];
if(!myObject.name)
{
myObject.name = name;
}
if(![imageUrl isEqualToString:myObject.imageURL]
|| !myObject.imageURL )
{
myObject.imageURL = imageUrl;
//TODO remove old image and get new one
//TODO2 in block callback after download myImageNameWithMD5 = imageNameWithMD5
}
if(![iconUrl isEqualToString:myObject.iconUrl]
|| !myObject.iconUrl)
{
myObject.iconUrl = imageUrl;
//TODO remove old image and get new one
//TODO2 in block callback after download myImageNameWithMD5 = imageNameWithMD5
}
}
I'm refreshing my tableView with fetchResultDelegate.
Now I have few questions. Would it be a proper way to get images in my //TODO sections with async download? Will fetchResultDelegate inform me that images are set if I will execute //TODO2 code? Or should I do it sync with thread which is adding those managedObject.
And finally how to stop imageDownload if app is killed?
Or should I do this download in my model class in myImageNameWithMD5 setter method?

I think that the real image should be seperated from the CoreData DataBase.
Because you might never actually going to show the user.
My approach is that only download the image when user is about to see it, and with third
party framework like SDWebImage, that couldn't be easier.
It handles the download, update, cache and display automatically.

This is similar to this.
However, I would not mix async operations with item creation.
I would create a "TODO" object (lets call it ImageToDownload) with a to-one relationship with the created/updated object.
I would create a manager that listen to the creation of such "TODO" objects and issue the download of images from it using NSOperations.
on successful creation you delete the "TODO" object.
This will allow you both to cancel image downloading when you need to, and also the download request is persisted and might be continued at a later time.

Ok, I've found the best solution (thanks to #Kyle Fang)
Im using SDWebImage Framework. In place where I'm filling my UIView with data I've used
[view.iconImageView setImageWithURL:[NSURL URLWithString:item.iconUrl]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
And in my NSManagedObjectCategory in place of //TODO I'm using this code:
[[SDImageCache sharedImageCache] removeImageForKey:myObject.iconUrl];

Related

NSBatchUpdateRequest not saving Transformable attribute

I have an entity (TestEntity) which contains a "Transformable" attribute which holds an object (MyObjectClass). On initial load, the transformable saves correctly; initialised as below:
TestEntity *test = (TestEntity *)[NSEntityDescription insertNewObjectForEntityForName:ENTITY[<Int>] inManagedObjectContext:temporaryContext];
test.transformableAttr = [[MyObjectClass alloc] initWithObject:obj];
However, when I fetch an object (I fetch as dictionary with NSDictionaryResultType) and update its "Transformable" attribute,
MyObjectClass *my_obj = ....
dict[#"transformableAttr"] = my_obj
it saves successfully but when I fetch it again I get nil for the "Transformable" attribute.
Now this only happens with "NSBatchUpdateRequest" because when I save using the MOC
TestEntity *test = ....
test.transformableAttr = updated_object
it saves successfully and I can access the updated attribute when fetched again.
Can anyone please explain? Does it mean that NSBatchUpdateRequest does not Transformable?
My NSBatchUpdateRequest code:
[context performBlock:^{
NSError *requestError = nil;
NSBatchUpdateRequest *batchRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:entity];
batchRequest.resultType = NSUpdatedObjectIDsResultType;
batchRequest.propertiesToUpdate = properties;
NSBatchUpdateResult *result = nil;
SET_IF_NOT_NIL(batchRequest.predicate, predicate)
#try {
result = (NSBatchUpdateResult *)[context executeRequest:batchRequest error:&requestError];
if (requestError != nil){
// #throw
}
if ([result.result respondsToSelector:#selector(count)]){
__block NSInteger counter = [result.result count];
if (counter > 0){
[managedObjectContext performBlock:^{
for(NSManagedObjectID *objectID in result.result){
NSError *faultError = nil;
NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&faultError];
if (object && faultError == nil) {
[managedObjectContext refreshObject:object mergeChanges:YES];
}
counter--;
if (counter <= 0) {
// complete
}
else{
// Wait.
}
}
}];
}
else{
// No Changes
}
}
else {
// No Changes
}
}
#catch (NSException *exception) {
#throw;
}
}];
The documentation doesn't seem to call out this particular scenario, but I'm not surprised that it doesn't work. An NSBatchUpdateRequest is described as [emphasis mine]:
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
Transformables work by converting to/from Data in memory. If your class conforms to NSCoding, the coding/decoding happens in memory, because SQLite doesn't know about NSCoding or your classes.
Your original assignment works because Core Data converts the value of transformableAttr to Data in memory and then saves the bytes of the Data to the persistent store. In the batch update, the objects aren't loaded into memory, so the transformation can't run, so the update doesn't work as you'd expect.
It's disappointing that Core Data doesn't make this clearer. Look in the Xcode console to see if it warns you about this. If it doesn't, please file a bug with Apple, because though I don't expect this to work, it's also not good for it to fail silently.
If you want to use batch updates, you'll have to convert your value in code before running the update. I'm not 100% certain how this will work but if your value conforms to NSCoding you'll start with
let transformedData: Data = NSKeyedArchiver.archivedData(withRootObject:transformableAttr)
What you do then is where I'm not sure. You might be able to use transformedData as the new value. Or you might have to access its bytes and use them somehow-- maybe using withUnsafeBytes(_:). You'll probably run into trouble because transformableAttr is not a Data, so it may get messy. It seems that batch updates aren't designed to work well with transformables.

Core Data multithreading--what am I doing wrong

I'll try to keep this brief but basically, I have an app that, in a certain mode, can near-continuously log location and other data, and snap photos (using AVFoundation) and store it all in Core Data. I discovered, as suspected, that all of this would need to be threaded...otherwise the UI gets extremely sluggish.
I have never attempted to combine Core Data with concurrency before so I read up on it as best I could. I feel like I understand what I'm supposed to do, but for someone reason it's not right. I crash with this error: "Illegal attempt to establish relationship "managedDataPoint" between objects in different contexts. I know what this means, but I thought what I have below would avoid this (I'm following what I've read)...since I get an Object ID reference from the main context, and use that to grab a new reference to the object and pass it to the "temp" context...but that isn't working as Core Data still claims I'm attempting to create a relationship across contexts (where?). Appreciate any help. Thank you!
-(void)snapPhotoForPoint:(ManagedDataPoint*)point
{
if (!_imageCapturer)
{
_imageCapturer = [[ImageCapturer alloc] init];
}
if (!_tempContext) {
_tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_tempContext.parentContext = self.managedObjectContext;
}
__block NSManagedObjectID* pointID = [point objectID];
[_tempContext performBlock:^{
NSError *error = nil;
Photo *newPhoto = [NSEntityDescription insertNewObjectForEntityForName:#"Photo" inManagedObjectContext:_tempContext];
UIImage *image = [_imageCapturer takePhoto];
newPhoto.photoData = UIImageJPEGRepresentation(image, 0.5);
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
newPhoto.managedDataPoint = tempPoint; // *** This is where I crash
if (![_tempContext save:&error]) { // I never get here.
DLog(#"*** ERROR saving temp context: %#", error.localizedDescription);
}
}];
}
Shouldn't
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
not be
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[_tempContext objectWithID:pointID];
Otherwise you are working with different contexts! Also you should check if objectID is a temporary ID and acquire a "final" one in case of.

Update Core Data inside my model class

What are the best practices on where to update my Core Data?
The first guy who worked on this project I'm working right now created all the Core Data related functions inside the ViewController, but I wanted to declare them inside the model classes (NSManagedObject subclass) to separate concerns.
The main function is a AFNetworking postPath that calls a web service and returns an array of objects to add/edit/delete. What I did was create a class method and do this AFNetwork call inside it:
+ (void)updateEbooksListWithSuccessBlock:(void (^)())successBlock andFailureBlock:(void (^)())failureBlock {
NSURL *url = urlSchema (urlWebServices, #"");
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSString *postPath = [NSString stringWithFormat:#"ws-ebooks-lista.php"];
[httpClient postPath:postPath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([operation isKindOfClass:[AFHTTPRequestOperation class]]) {
NSDictionary *result = [[responseObject objectFromJSONData] retain];
bool success = statusDoRetornoDoWebService(result); //Function that checks if the return was successful
//Configura o Core Data
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSManagedObjectContext *localManagedObjectContext = [[NSManagedObjectContext alloc] init];
[localManagedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Ebooks" inManagedObjectContext:localManagedObjectContext];
NSPredicate *filterPredicate;
[request setEntity:entity];
if (success) {
NSArray *ebookInfos = [result objectForKey:#"saida"];
Ebooks *ebook;
NSManagedObject *objectInsert;
for (NSDictionary* ebookInfo in ebookInfos) {
filterPredicate = [NSPredicate predicateWithFormat:#"ebooks_id == %#",[ebookInfo valueForKey:#"id_ebook"]];
[request setPredicate:filterPredicate];
ebook = [[localManagedObjectContext executeFetchRequest:request error:&error] lastObject];
objectInsert = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:localManagedObjectContext];
if (ebook) {
if (![[ebookInfo valueForKey:#"excluido"] isEmpty]) {
//Delete Ebook
} else {
//Update Ebook
}
} else {
//Add Ebook
}
if (![localManagedObjectContext save:&error]) {
//Log Error
}
[objectInsert release];
}
}
[request release];
[localManagedObjectContext release];
}
[successBlock invoke];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//Failure
[failureBlock invoke];
}];}
And it works fine while the app is running, but if I close it (through Xcode) and open it again, the changes aren't saved. I tried not using the "parent context" way and just using the AppDelegate managed object context (since AFNetworking callbacks always runs on the main queue) but no success: the data is not persisted. Why is that? Am I doing something wrong? Is it bad practice? Should I leave everything in the View Controller the way it was?
Thanks!
I think it is a bad idea to have too much logic that ultimately relates to your data model into your entity classes. These tasks simply do not belong there. The entity classes should focus only on what they encapsulate: the entity instances themselves.
To illustrate: think of a class that represents a number (like NSNumber). It think it is not convenient to extend it to give you, say, an array of all even numbers within a certain limits, or the nth member of the Fibonacci series. It seems unsound to have a number class be responsible for saving itself to a file, or retrieving information from the web.
For these and similar reasons, I believe the fetching and saving of Core Data entities belongs into controllers, not entity classes. Remember, one of the basic ideas behind the MVC (model-view-controller) pattern is that the controller manipulates the model or asks it for information, not that the model manipulates itself.
I speculate that your troubles are derived mainly from not separating the various functional aspects of your application sufficiently (data model, persistence, network operations, user interactions).
ugh... what I would do is make very naked NSManagedObject subclasses... then extend them with categories, that way when you regenerate your classes from the updated model you don't have to try to merge in all of your custom logic.
also the custom logic belongs in the model, the model contains the category or class extension.
so take that crap out of the View Controllers and put it in an easily maintainable category or several categories if it is warranted.

Core data update or create find updated

I have core data entity which contains fields such as name(unique), imageURL and image (saving image as data). Im downloading this data from web API which I have no control of (data in JSON).
I have to check every week if there were changes on API side and update my local database.
Sometimes its changing imageURL property and I have to detect that and download new image and remove old one. Any idea how to implement that (I would be glad for piece of code).
I would have thought it was fairly straight forward.
You are able to download the images when you first get the item.
So now have a check something like...
if currentImageURL is different from newImageURL download the image.
EDIT - To explain how it should work
Assuming you've processed the JSON and now you have an NSArray of NSDictionaries...
You would do something like this...
//I'm assuming the object is called "Person"
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
for (NSDictionary *personDictionary in downloadedArray) {
// You need to find if there is already a person with that name
NSPredicate *namePredicate = [NSPredicate predicateWithFormat:#"name = %#", personDictionary[#"name"]];
[request setPredicate:namePredicate];
// use whichever NSManagedObjectContext is correct for your app
NSArray *results = [self.moc executeFetchRequest:request error:&error];
Person *person;
if (results.count == 1) {
// person already exists so get it.
person = results[0];
} else {
// person doesn't exist, create it and set the name.
person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.moc];
person.name = personDictionary[#"name"];
}
// check the image URL has changed. If it has then set the new URL and make the image nil.
if (![personDictionary[#"imageURL"] isEqualToString:person.imageURL]
|| !person.imageURL) {
person.imageURL = personDictionary[#"imageURL"];
person.image = nil;
}
// now download the image if necessary.
// I would suggest leaving this here and then wait for the image to be accessed
// by the UI. If the image is then nil you can start the download of it.
// now save the context.
}

iOS CoreData Background Thread Fetching in cellForRowAtIndexPath

I have a tableview in my app that contains a NSFetchedResultsController to load in some CoreData objects.
As the table builds in cellForRowAtIndexPath:, for each cell I must do a fetch to get some other info from another object.
The table is filled with UserTasks, and I must get some info from a UserSite (UserTask contains a siteID attribute)
I am getting the UserSite info in a background thread, and using a temporary context. It works fine, but it still wants to lag the UI a bit when scrolling.
Site *site = [_scannedSites objectForKey:task.siteID];
if(!site)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
Site *site2 = [task getSiteWithContext:temporaryContext];
if(site2)
{
[ad.managedObjectContext performBlock:^{
Site *mainContextObject = (Site *)[ad.managedObjectContext objectWithID:site2.objectID];
[_scannedSites mainContextObject forKey:task.siteID];
}];
dispatch_async(dispatch_get_main_queue(), ^{
Site *newSite = [_scannedSites objectForKey:task.siteID];
cell.lblCustName.text = newSite.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", newSite.siteAddressLine1, newSite.siteCity, newSite.siteState];
cell.lblPhone.text = [self formatPhoneNum:newSite.phone];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = #"";
cell.lblAddr.text = #"";
cell.lblPhone.text = #"";
});
}
});
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
As you can see, if you dont already have the UserSite info for a task in _scannedSites, a background thread gets kicked off which gets the UserSite for that task, stores it, and then on the main thread fills in the details.
Like I said there is a pretty annoying lag when scrolling... which I hoped to avoid by doing the work in the background.
Am I going about this the wrong way?
Thanks, any advice is appreciated.
EDIT
I created a relationship in CoreData and I am now using that in cellForRowAtIndexPath. If it does not exist yet, I create it. This is working much better.
Site *site = task.site;
if(!site)
{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
[temporaryContext performBlock:^{
Site *tempContextSite = [task getSiteWithContext:temporaryContext];
[ad.managedObjectContext performBlock:^{
Site *mainManagedObject = (Site *)[ad.managedObjectContext objectWithID:tempContextSite.objectID];
task.site = mainManagedObject;
NSError *error;
if (![temporaryContext save:&error])
{
}
[ad.managedObjectContext performBlock:^{
NSError *e = nil;
if (![ad.managedObjectContext save:&e])
{
}
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = mainManagedObject.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", mainManagedObject.siteAddressLine1, mainManagedObject.siteCity, mainManagedObject.siteState];
cell.lblPhone.text = [self formatPhoneNum:mainManagedObject.phone];
});
}];
}];
}];
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
If UserTask relates to UserSite, the usual Core Data approach would be to create a relationship between the two and then use that relationship at run time. So, UserTask would have a property named site, and you'd just ask a specific instance for the value of that property. An ID attribute might still exist but would only be used when syncing with some external data store (like a server API).
Storing IDs and looking up objects like this is a fundamentally awkward approach that's pretty much designed to do a lot of unnecessary work at run time. It avoids all of the conveniences that Core Data tries to provide, doing things the hard way instead. Doing this work while the table is scrolling is also about the worst possible time, because it's when a performance issue will be most noticeable.
If you must do it this way for some reason, you could optimize things by looking up all of the UserSite instances in advance instead of while the table is scrolling. If you know all of the UserTask instances, go get all the sites in one call when the view loads.
It is a bad idea to send of asynchronous tasks in cellForRowAtIndexPath:. If the user scrolls there are going to be a whole bunch of threads created which are maybe not even necessary.
It would be much better to have a background process that fetches the information you want and then notifies the UI to update itself if needed. This is pretty standard stuff, you will find many examples for solid implementations easily.

Resources