Core data update or create find updated - ios

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.
}

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.

NSBatchDeleteRequest leaving bad relationships

I have a Project object that has a to-many relationship to Image, the delete rules are set up as Cascade for project.images and Nullify for image.project. In this case I need to clear out the attached images but leave the project itself intact. There are a lot of images so I want to use a batched delete to get them all in one go.
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Image"];
request.predicate = [NSPredicate predicateWithFormat:#"project = %#", project];
NSBatchDeleteRequest *delete = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
delete.resultType = NSBatchDeleteResultTypeObjectIDs;
NSError *error;
NSBatchDeleteResult *result = [context.persistentStoreCoordinator executeRequest:delete withContext:context error:&error];
if (error) {
// todo
}
[NSManagedObjectContext mergeChangesFromRemoteContextSave:#{NSDeletedObjectsKey : [result result]} intoContexts:#[context]];
NSLog(#"images: %#", project.images); // Still has all the images (they are now flagged as isDeleted)
[context save:&error];
NSLog(#"images: %#", project.images); // Still has all the images...
According to the docs the mergeChangesFromRemoteContextSave line should take care of updating the context but this doesn't seem to happen.
One more thing, I know I can set project.images = nil and this does the job, but can't be used in a case when I am only deleting a subset of the images.
Edit: I have investigated further and have more info.
The issue occurs when project.images are faults. If I force them all to fault in then the relationships will successfully updated when the delete goes through. If I leave them as faults then the relationship is untouched and will now point to non-existent objects.
In mergeChangesFromRemoteContextSave try including the NSUpdatedObjectsKey with the project's objectID.
[NSManagedObjectContext mergeChangesFromRemoteContextSave:#{NSUpdatedObjectsKey : #[project.objectID], NSDeletedObjectsKey : [result result]} intoContexts:#[context]];`

Core data - countforfetch always returns 1 for empty database

I have seen this link where countforfetch is always 1. But doesn't give the solution.
When i do a fetch request as given in the link it gives me the data i was about to save every time. So since its already present it wont re-save. So the database is empty. But surprisingly the data comes on the table.
This seems like a very weird behaviour. Can some please help ?
here is my code
NSFetchRequest *fetchRequest12 = [[NSFetchRequest alloc] init];
NSError *error;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"orderNumber = %#",orderList.orderNumber];
[fetchRequest12 setPredicate:predicate];
[fetchRequest12 setEntity:[NSEntityDescription entityForName:#"OrderList" inManagedObjectContext:appDelegate.managedObjectContext]];
NSLog(#"The fetch was successful! %#", [appDelegate.managedObjectContext executeFetchRequest:fetchRequest12 error:&error]);
if ([appDelegate.managedObjectContext countForFetchRequest:fetchRequest12 error:&error] ==0 ) { // Somethings to be done
}
Use setIncludesPendingChanges: to NO if you want the fetch request to ignore any changes that you have made in the MOC but not yet saved. By default all unsaved changes are fetched (hence you see unsaved changes displayed in your UI).

Asyc image download with core data

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

Saving and Deleting NSManagedObject & NSManagedObjectContext

Three Questions but they are all related. If you like I can divide them into three questions so that you can more credits. Let me know if you'd like for me to do that.
I have the following code that allows me to access NSManagedObject
self.managedObjectContext = [(STAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSError *error;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"LetsMeet" inManagedObjectContext:managedObjectContext]];
NSArray *objectArray = [managedObjectContext executeFetchRequest:request error:&error];
if(objectArray.count==0){
letsMeet = (LetsMeet *) [NSEntityDescription insertNewObjectForEntityForName:#"LetsMeet" inManagedObjectContext:managedObjectContext];
} else{
letsMeet = (LetsMeet *)[objectArray objectAtIndex:0];
}
The code above allows me to save and retrieve attributes. i.e. I can access letsMeet.attribute to save and fetch.
Question 1: How do I delete and start a brand new managedObjectContext. i.e. User has a form that he's been filling out between the scenes. Everything is saved to CoreData from each scene as the user hits the Next button on the navigation Controller. After going through several screens, the user wants to cancel the form. At this point I would like to delete everything that has been saved thus far. Code example please.
Question 2: Lets say the user gets towards to end of the form and decides to save the form for later retrieval. How do I save a copy of the entire form as one object in Core Data. Code example please.
Question 3: How do I retrieve that saved object from Core Data at a later time and display what all the user had saved? Code example please.
To delete you just need to delete letsMeet object from NSManagedObjectContext.
NSError *error;
[managedObjectContext deleteObject:letsMeet];
[managedObjectContext save:&error];
Since you always have only one object, getting the reference of letsMeet is not a problem. You can do as you did in your code.
Update:
And you don't need to delete the managed object context. It just a space to deal with your objects. More explanation at the end of question.
2. If the LetsMeet entity is modeled in a way that all the form elements are attributes of LetsMeet, when you save the managedObjectContext after creating a LetsMeet object as you have done in code, this will be saved as a single object.
3.You already know how to retrieve an object as thats what you are doing in the code. Everything becomes easy as you are only using one object.
In the case of multiple objects to get a the unique object, you should either implement a primary key,(maybe formID, i.e; add another attribute to LetsMeet) or you should know what the objectId of each object is and then set the predicate of your fetch request accordingly.
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:letsMeet];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"formId like %#", formId];
[request setPredicate:predicate];
NSArray *resultsArray =[managedObjectContext executeFetchRequest:request error:&error];
If your formId is unique, this will return you a single object array.
But if you are using core-data for only handling one object, you could've used NSUserDefaults or write to a plist File to do this. This is kind of overkill.
Update:
To get the objectId of a NSManagedObject:
[letsMeet objectId];
ManagedObjectContext is like a whiteboard. The object you have inside the array, the object inside managed object context, its all the same. You can change the objects, add object, delete object etc. Only thing is whatever is the current state of the object(s) when you do a [managedObjectContext save:] , that is written to disk.

Resources