I'm working with Core Data for the first time, with the Stanford iOS app development course as a guide. I pretty much copied the code from the demo app (of course I adjusted it to my needs), but I'm having two problems currently.
My app is a map view which on the tap of a button presents a modal view controller. This modal view checks whether a UIManagedDocument was created. If not, it creates one and inserts the data. This data is coming from a property list (258 items, so nothing too excessive). If it was created already (by previously displaying that view), if my logic holds, it should be safe to assume it also has content because the NSManagedObjects are created at the same time a document is created. The first run works perfectly fine, the table loads and all my data is correctly displayed.
However, when I dismiss and then re-display my modal view, the table stays empty. I'm checking the document's state, which is UIDocumentStateNormal, so querying it should be fine. But it isn't: my fetchedResultsController returns 0 rows. If I understand UIManagedContext correctly, the behavior I'm experiencing could be caused by a wrong/different context, but I make sure that: 1) I pass my document (not just the context) to the modal view in prepareForSegue:sender, and 2) I pass my document with the context back to the presenting view when the modal view is being dismissed. That's why I think it's probably not the context, but something else.
One other thing: inserting the 258 records when the app is first launched is fast enough in the simulator. However, on my phone it could take a whole 13 seconds. The insertion code is shown below (modified for readability):
+ (Department *)departmentName:(NSString *)name
withAttributes:(NSDictionary *)attributes
inContext:(NSManagedObjectContext *)context {
Department *department = [NSEntityDescription insertNewObjectForEntityForName:#"Department" inManagedObjectContext:context];
department.name = name;
NSArray *informationElements = [attributes objectForKey:#"information"];
for (int i = 0; i < [informationElements count]; i++) {
NSString *informationValue = [[informationElements objectAtIndex:i] objectForKey:#"value"];
if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"phone"]) {
department.phone = informationValue;
} else if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"email"]) {
department.email = informationValue;
} else if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"web"]) {
department.website = informationValue;
}
}
return department;
}
To be clear: this code works just fine, but it's really slow. It's encapsulated in a method which is called exactly 258 times. informationElements has at most three elements, which means there are 258 * 3 = 774 loops maximum. In reality it's much less than that, but even if it were 774, that shouldn't take 13 seconds, right?
The snippet below shows the initialization of UIManagedDocument:
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.database.fileURL path]]) {
[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self setupFetchedResultsController];
[self fetchDepartmentsIntoDocument:self.database];
}];
} else if (self.database.documentState == UIDocumentStateClosed) {
[self.database openWithCompletionHandler:^(BOOL success) {
[self setupFetchedResultsController];
}];
} else if (self.database.documentState == UIDocumentStateNormal) {
[self setupFetchedResultsController];
}
fetchDepartmentsIntoDocument reads the property list and then runs a loop which calls departmentName:withAttributes:inContext for every property list item.
If anyone could provide me with some help, it will be much appreciated!
For the speed issue, I would look into using predicates; that should speed things up a great deal!
Predicates make it so that the context returns only the values needed based on whatever criteria you choose. The reason they are faster is because it does not have to convert each stored entity object into a managed object, rather it can pull straight from the property, which speeds up comparisons drastically.
When you're inserting Department objects into the context, are you saving for each object? Inserting is relatively cheap, but saving (i.e. -[NSManagedobjectContext save:]) is expensive (since the database has to do locking and file I/O).
Also, on a more stylistic note, you can do
for (NSDictionary *element in informationElements) {
NSString *informationValue = [element objectForKey:#"value"];
if ([[element objectForKey:#"description"] isEqualToString:#"phone"]) {
department.phone = informationValue;
} else if ([[element objectForKey:#"description"] isEqualToString:#"email"]) {
department.email = informationValue;
} else if ([[element objectForKey:#"description"] isEqualToString:#"web"]) {
department.website = informationValue;
}
}
to iterate through your array of dictionaries.
Related
I'm having a threading issue loading images in a collectionview where the data is coming from cloudkit. I know this is a threading/blocking issue because before I implemented CK, I dumped some images in a folder on my desktop and read/parsed them from there and had no issue. With CK, I just created a handful of records via the dashboard and I'm successfully getting the expected records returned and use the images from those results to populate the CV cells. I store the CK query results in an array and use the size of that array to set the numberOfItemsInSection delegate.
Here's the issue...in the numberOfItemsInSection delegate method, I'm calling the model class, which executes the CK query. Since that is obviously a network call, I put that in a background thread. From logging, I can see the query execute and the results come back very quickly - within 2-3 seconds. However, the CV cells never display and I don't see the custom cell get initialized (via logging). But if I tap the camera button and take a photo, which I've implemented, I take the resulting image and add it to the array, then call reloadData on the CV and all the cells (and images) appear, including the new image just taken with the camera.
By accident, I found out a hack that somewhat works, which is calling reloadData on the CV inside the background thread of the numberOfItemsInSection delegate method. As a result, I thought I might have stumbled on to the solution by switching back to the main thread when calling reloadData, but that put it in a sort of endless loop of continuously calling the numberOfItemsInSection method and cellForItemAtIndexPath and made it to where it lagged to a point that you could barely scroll and tapping on any of the cells wouldn't do anything.
At this point, after trying many, many various things, I'm at a complete loss on how to fix this. I know this is probably a pretty easy solution as it's very common to load images asynchronously to populate a collectionview or tableview. Can someone please provide some guidance? Thanks in advance!!!
#property (nonatomic) NSInteger numberOfItemsInSection;
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSLog(#"***numberOfItemsInSection***");
dispatch_queue_t fetchQ = dispatch_queue_create("load image data", NULL);
dispatch_async(fetchQ, ^{
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self.myCollectionView reloadData]; // should be done on main thread!
});
NSLog(#"numberOfItemsInSection: %ld", (long)self.numberOfItemsInSection);
return self.numberOfItemsInSection;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell"; // string value identifier for cell reuse
ImageViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
NSLog(#"cellForItemAtIndexPath: section:%ld row:%ld", (long)indexPath.section, (long)indexPath.row);
cell.layer.borderWidth = 1.0;
cell.layer.borderColor = [UIColor grayColor].CGColor;
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
ImageData *imageData = [self.imageLoadManager imageDataForCell:indexPath.row]; // maps the model to the UI
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData.imageURL.path) {
cell.imageView.image = [UIImage imageWithContentsOfFile:imageData.imageURL.path];
[cell setNeedsLayout];
} else {
// if imageURL is nil, then image is coming in from the camera as opposed to the cloud
cell.imageView.image = imageData.image;
[cell setNeedsLayout];
}
});
return cell;
}
before returning self.numberOfItemsInSection you should wait until the async call is finished. You can do that using semaphores. But then why are you doing this async? You are just getting the count of an array. And then you shouldn't reloadData there. Where do you start your CloudKit query? are you doing that onViewDidLoad? That is also an async operation. When that completes just doe a reloadData of your collectionView. Besides that doing this would be enough:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.imageLoadManager.imageDataArray count];
}
If you really want to use async there, then you do have to wait for the result. You could change your code to something like:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSLog(#"***numberOfItemsInSection***");
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t fetchQ = dispatch_queue_create("load image data", NULL);
dispatch_async(fetchQ, ^{
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self.myCollectionView reloadData]; // should be done on main thread!
dispatch_semaphore_signal(sema);
});
NSLog(#"numberOfItemsInSection: %ld", (long)self.numberOfItemsInSection);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return self.numberOfItemsInSection;
}
And then why do you go to the main queue in cellForItemAtIndexPath? It's already executed on the main queue.
I'm using AFNetworking and MagicalRecord (the current develop branch) and I'm trying to figure out how to import a lot of objects which are dependent on each other. Each resource/entity has multiple pages worth of downloads. I have a class managing the downloads for a given entity and saving them using MagicalDataImport (which has been amazing).
I believe my issue is that the imports aren't happening on the same thread. So I think what is happening is:
In one thread, EntityA is getting saved properly and propagated to the parent entity.
Then in another thread, EntityB is being saved, and along with it it's relationship to EntityA is built. That means a blank (fault?) object is being created. Then when it gets propagated to the parent entity, I believe EntityA is overwriting the EntityA that is there. Thus I'm left with some objects that don't have all of the attributes.
At least, I think that is what is happening. What I'm seeing via the UI is actually that the relationships between entities aren't always built correctly.
My end goal is to get the entire download/import process to be done in the background, not effecting the UI at all.
Here is my AFJSONRequest:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self saveResources:[JSON objectForKey:#"data"]];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
DLog(#"%#",error.userInfo);
[self.webService command:self didFail:error.localizedDescription];
}];
[operation setQueuePriority:self.priority];
And it calls saveResources::
- (void)saveResources:(NSArray*)resources {
BOOL stopDownloads = [self stopDownloadsBasedOnDate:resources];
if ([resources count] > 0 && !stopDownloads){
self.offset = #([offset intValue] + [resources count]);
[self send];
}
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *blockLocalContext) {
[self.classRef MR_importFromArray:resources inContext:blockLocalContext];
} completion:^(BOOL success, NSError *error) {
if (error){
// ... handle errors
}
else {
// ... handle callbacks
}
}];
}
This kicks off another download ([self send]) and then saves the objects.
I know by default AFNetworking calls the callback in the main queue, and I've tried setting the SuccessCallbackQueue/FailureCallbackQueue to my background thread, but that doesn't seem to solve all the issues, I still have some relationships going to faulted objects, though I think I do need to do that to keep everything going in a background thread.
Is there anything else I need to call in order to properly propagate these changes to the main context? Or is there a different way I need to set this up in order to make sure that all the objects are saved correctly and the relationships are properly built?
Update
I've rewritten the issue to try to give more clarification to the issues.
Update
If you need more code I created a gist with (I believe) everything.
I ended up having this exact same issue a few days ago. My issue was I had received a customer record from my API with AFNetworking. That customer could have pets, but at this point I didn't have the petTypes to correspond to the customers pet record.
What I did to resolve this was create a transformable attribute with an NSArray which would temporarly store my pets until my petTypes were imported. Upon the importation of petTypes I then triggered an NSNotificationCenter postNotification (or you can just do the pet import in the completion).
I enumerated through the temporary transformable attribute that stored my pet records and then associated the with the petType
Also I see you are doing your import inside of a save handler. This is not needed. Doing your MR_importFromArray will save automatically. If you are not using an MR_import method then you would use the saveToPersistentStore methods.
One thing is I don't see where you are associating the relationships. Is EntityB's relationship to EntityA being sent over via JSON with the EntityA objecting being in EntityB?
If so then this is where the relationship is getting messed up as it is creating / overwriting the existing EntityA for the one provided in EntityB. My recommendation would be to do something like this.
NSArray *petFactors = [responseObject valueForKeyPath:#"details.items"];
NSManagedObjectContext *currentContext = [NSManagedObjectContext MR_context];
Pets *pet = [Pets MR_findFirstByAttribute:#"id" withValue:petId inContext:currentContext];
pet.petFactors = nil;
for (id factor in petFactors) {
[pet addPetFactorsObject:[PetFactors MR_findFirstByAttribute:#"id" withValue:[factor valueForKey:#"factorId"]]];
}
[currentContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED PET FACTORS");
[[NSNotificationCenter defaultCenter] postNotificationName:kPetFactorsSavedSuccessfully object:nil];
} else {
NSLog(#"Error: %#", error);
}
}];
I'm putting this as an answer, though I'm not 100% sure if this is your issue or not. I think the issue stems from your localContext. Here is a sample web request method from an app we wrote that uses data importing, you may be able to use it as an example to get yours working.
Note that the AFNetworking performs its completion block on the main thread, then the MagicalRecord saveInBackground method switches back to a background thread to do the importing and processing, then the final MR completion block performs the handler block on the main thread again. The localContext that's used to import is created/managed by the saveInBackground method. Once that method is complete the context is saved and merged with the app's main context and all the data can then be accessed.
- (void)listWithCompletionHandler:(void (^)(BOOL success))handler{
[[MyAPIClient sharedClient] getPath:#"list.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject){
NSString *statusString = [responseObject objectForKey:#"status"];
// Handle an error response
if(![statusString isKindOfClass:[NSString class]] || ![statusString isEqualToString:#"success"]){
// Request failure
NSLog(#"List Request Error: %#", statusString);
NSLog(#"%#", [responseObject objectForKey:#"message"]);
if(handler)
handler(NO);
return;
}
NSArray *itemsArray = [responseObject objectForKey:#"items"];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
// Load into internal database
NSArray *fetchedItems = [Item importFromArray:itemsArray inContext:localContext];
NSLog(#"Loaded %d Items", [fetchedItems count]);
} completion:^{
if(handler)
handler(YES);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"Fail: %#", error);
if(handler)
handler(NO);
}];
}
I am creating a table view controller for an app that manages position assignments for a team. The sections with headers for defense, center, and offense for example will have names in them if a position Entity exists with that positionProperty. If that person is removed though through swiping, they become an alternate entity with same positionProperty.
I am trying to have the alternates for each position display when the edit button is tapped. Much like extra contact details appear when you edit a contact in the contacts app.
I have a fetchedResultsController returning the parent entity for alts/positions keyed by the positionProperty to define sections. (This may be the wrong way to do this... I am new to Core Data).
In setEditing:WithAnimation I have done the following. Attempting to search my fetched results and if any objects are of type Alternate, display that row. So in the enumeration, if it is type alternate I tried to call IndexPathForObjects:alt. This just returned nil...
if(editing){
[self.tableView beginUpdates];
for (MCAlternate *alt in fetchedResultsController.fetchedObjects) {
if ([alt isKindOfClass:[MCAlternate class]]) {
NSLog(#"The alternate is: %#", alt);
// This is where the error is trying to get indexPathForObject
NSIndexPath *index = [fetchedResultsController indexPathForObject:alt];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[index] withRowAnimation:UITableViewRowAnimationLeft];
}
}
[self.tableView endUpdates];
}
I have checked if the object exists in the results. It does. I have also tried called the getindexpath in a place with the object was just created by calling objectAtIndexPath and it still came back nil.
any suggestions are appreciated, Thanks!
Maybe you are not using your fetched results controller as it is foreseen. Your frc should fetch all the data to populate the table view.
You can still achieve the dynamic table content by tweaking your table view datasource methods. Suppose you are fetching the entity "Position" and the position object in question has a to-many attribute of entity "MCAlternate" called "alternates". You would then expand a certain section as follows:
-(void)expandSectionForPosition:(Position *)position {
int row = 0;
int section = [[frc indexPathForObject:position] section];
for (MCAlternate *alt in position.alternates) {
// update your datasource - e.g. by marking the position as
// "expanded"; make sure your numberOfRowsInSection reflects that
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath
indexPathForRow:i++ inSection:section]]
withRowAnimation:UITableViewRowAnimationLeft];
}
}
Your class check is not logical - you are casting as MCAlternate in the for loop anyway. Your call or getting the index path fails, because you typically do not have a separate fetched results controller.
I have an entity called Practice and I use a View Controller called SelectorViewController to select one of the practices, selectedPractice. I then return selectedPractice to a view Controller called RegularViewController where I display some of the selectedPractice attributes. All of this works fine. However the app has a number of other View Controllers which can be reached by modal segues from instances of RegularViewController. As a result, if I leave and then come back to RegularViewController, selectedPractice is reset as null. I would also like to save selectedPractice so that it is available at app initialisation if it has previously been set in SelectorViewController. How do I achieve this by making selectedPractice persistent across the app, and available at runtime?
Regards
Thanks to the post above, which was great, I managed to sort it. Here is my code, which may be very clumsy, but it works.
Firstly, as I loaded the fetchedObjects into a PickerView in SelectorView Controller, I set an attribute "isSelectedPractice" to "NO" with the following code:
for (Practice *fetchedPractice in [self.fetchedResultsController fetchedObjects]) {
[fetchedPractice setValue:#"NO" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
I then identified for the selected Practice:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
Practice *practice = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
self.selectedPractice = practice;
NSLog(#"The '%#' practice was selected using the picker", self.selectedPractice.name);
}
as the view Segue'd back to RegularViewController I set the isSelectedPractice attribute for selectedPractice to YES. I kept it this late as I didn't want more than one selection in the PickerView to result in multiple objects with isSelectedPractice YES.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SavedPractice Segue"])
{
[self.selectedPractice setValue:#"YES" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
NSLog(#"Setting SelectedPractice as '%#' in RegularViewController with isSelectedPractice as '%#'",self.selectedPractice.name,self.selectedPractice.isSelectedPractice );
RegularViewController *rvc= segue.destinationViewController;
rvc.delegate = self;
rvc.selectedPractice = self.selectedPractice;
}
else {
NSLog(#"Unidentified Segue Attempted!");
}
}
I then set the following Predicate in the setupFetchedResultsController method of RegularViewController:
request.predicate = [NSPredicate predicateWithFormat:#"isSelectedPractice = %#", #"YES"];
Many thanks for the help
Without seeing your actual project, one way I know will work but might be a little too round a bout would be to add an attribute "isSelectedPractice" to your entity. You could make it a BOOL, but I've had mixed results with BOOL's in Core Data, I prefer to just leave it as a NSString and set it to "yes" or "no". Then when you pull it down, modify it or add it to core Data as a entity with isSelectedPractice set to "yes". Then in your other controllers, do a
if (self.managedObjectContext == nil) {
self.managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
then do a fetch request to get entities with a predicate which is looking for isSelectedPractice equaling "yes". If you need actual code samples on how to do this let me know and I'll edit them in.
I have created few entities in context for saving it in db using
AppCalendarEntity *appCalendar = [AppCalendarEntity getInstanceWithManagedDocument:manageDocument];
After adding a few entities I execute flowing fetch request
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"AppCalendarEntity"];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
It returns me the result including only the entities I have added in context using first command and NOT the entries already saved in database. I have made sure that at this stage the document state is UIDocumentStateNormal.
When I add this line to already open document (UIDocumentStateNormal) it returns me the expected result, i.e. it fetch results from db as well as memory context which has not yet been saved to db.
[managedDocument openWithCompletionHandler:^(BOOL success)
{
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"AppCalendarEntity"];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
}
My question is
1- I expect that the result of query should be the same in both cases. Why it is not so in the above case.
2- To me if document state is UIDocumentStateNormal I should not be calling "openWithCompletionHandler" in context to open the document. In this particular scenario what difference it is making in NSFetchRequest which gives me the desired result after adding this.
Please let me know if I'm getting wrong
Here is the complete code
This is the complete code of the function
+ (void ) saveCalendarArrayInDbIfItAlreadyDoesNotExist : (NSArray*) appCalendarArray managedDocument: (UIManagedDocument*) managedDocument completionBlock : ( void(^) (NSArray* ObjectSavedSuccesfully, NSError *InternalError)) handler
{
// i dont know why i have to do it :( if i dont add openWithCompletionHandler my query doesnt fetch result from db rather just do query in-memory context and not db
[managedDocument openWithCompletionHandler:^(BOOL success)
{
void (^completionHandler)(NSArray* , NSError* );
completionHandler = [handler copy ];
NSError *error = nil;
NSMutableArray *array = [[NSMutableArray alloc] init];
for (id appCalendar in appCalendarArray) {
if([appCalendar isKindOfClass:[AppCalendarEntity class]])
{
AppCalendarEntity *appCalendarEntity = (AppCalendarEntity*) appCalendar;
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"MyEntity"];
requestToSeeIfCalendarWithIdExist.predicate = [NSPredicate predicateWithFormat:#"identifier = %#", appCalendarEntity.identifier];
NSError *InternalError = nil;
[requestToSeeIfCalendarWithIdExist setShouldRefreshRefetchedObjects:YES];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
// "result" is different when we encapsulate it in openWithCompletionHandler and when we don't…….MY PROBLEM
if(result == nil)
{
// return error
}
// 1 object always return that depict the in memory(context) object we created but not saved. I expect it should be zero because no object has yet been saved to database..
else if(result.count > 1)
{
[managedDocument.managedObjectContext deleteObject:appCalendar];
}
else
{
[array addObject:appCalendarEntity];
}
}
else
{
// error handling
}
}
if (error != nil)
{
completionHandler (nil, error);
return;
}
// saving all the objects
[ managedDocument updateChangeCount:UIDocumentChangeDone ];
}
When using UIManagedDocument, you do not call save on the MOC because it implements auto-save. however, it needs to be told that an auto-save should take place at some point in the future.
Get rid of that call to openWithCompletionHandler in that function (I know it was just there for purposes of debugging this problem).
Replace
[managedDocument.managedObjectContext save:&InternalError ]
with
[managedDocument updateChangeCount:UIDocumentChangeDone];
This will notify the document that it can now be saved.
EDIT
First, I think you should get rid of the debugging hacks. You can add NSLog or NSAssert, but the rest of that stuff just makes it hard to tell why you want, and confuses the real issue.
Second, what is your real goal here? I can see the name of the method, and I can see the code, but they do not match.
There is so much "cruft" here, it is hard to understand your problem. I am going to repost your code, along with an edit to remove the "open" stuff, and annotate it with questions as code comments.
Hopefully, this change will help you solve your problem.
// First, the method name seems to indicate that some objects will be added
// to the database. however, the only database work in this method is removal.
// I don't get it.
+ (void ) saveCalendarArrayInDbIfItAlreadyDoesNotExist : (NSArray*) appCalendarArray managedDocument: (UIManagedDocument*) managedDocument
{
NSError *error = nil;
NSMutableArray *array = [[NSMutableArray alloc] init];
for (id appCalendar in appCalendarArray) {
if([appCalendar isKindOfClass:[AppCalendarEntity class]]) {
// OK, we are filtering the array of objects. We are only interested in
// objects of type AppCalendarEntity, and are going to use its identity
// property to look for objects of type MyEntity.
// What is the relationship between AppCalendarEntity and MyEntity?
AppCalendarEntity *appCalendarEntity = (AppCalendarEntity*) appCalendar;
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"MyEntity"];
requestToSeeIfCalendarWithIdExist.predicate = [NSPredicate predicateWithFormat:#"identifier = %#", appCalendarEntity.identifier];
NSError *InternalError = nil;
[requestToSeeIfCalendarWithIdExist setShouldRefreshRefetchedObjects:YES];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError];
// OK, now we just got a result from searching for a MyEntity, where
// its identifier is the same as the appCalendarEntity.
if(result == nil)
{
// return error
}
// 1 object always return that depict the in memory(context) object we created but not saved. I expect it should be zero because no object has yet been saved to database..
else if(result.count > 1)
{
// I am extremely confused by this code. First, why are you
// checking for more than 1 object? The method name indicates
// you are going to insert something. Furthermore, you are only
// deleting one object. How many do you expect? Also, why are
// you deleting an appCalendar? You were searching for a MyEntity.
// If an appCalendar is a MyEntity, then that's terrible naming.
// Furthermore, it would explain why you are finding it...
// because you create entities by inserting them in a MOC to
// begin with!
[managedDocument.managedObjectContext deleteObject:appCalendar];
}
else
{
// Even more confusion. You are adding this object to an internal
// array, not the database. Furthermore, you are doing it if there
// are either 0 or 1 MyEntity objects in the database with matching
// identifier.
[array addObject:appCalendarEntity];
}
}
}
// saving all the objects
// OK - but the only thing being saved are the ones you deleted...
[ managedDocument updateChangeCount:UIDocumentChangeDone ];
}
Finally, if my hunch is correct, and the calendar objects are actually MyEntity objects, they are already in the MOC - because that's how they get created. When you do a fetch, you can force the search to ignore pending changes (as noted in one of my previous comments) and only accept saved changes.
If you want to ignore pending changes,
fetchRequest.includesPendingChanges = NO;
#Jody Problem has been resolved and thank you for giving time to this question.
First let me address your confusions
1- Yes function is intended to save in the database and it is a helping function. The parameter "appCalendarArray" being passed to this function consist of entities that has already been created in context. I intentionally eliminated the logic since it involves communicating with external apis, parsing json etc etc. The code required for inserting entities in context has already been included in first part of the question.
AppCalendarEntity *appCalendar = [AppCalendarEntity getInstanceWithManagedDocument:manageDocument];
2- I eliminate the entities from context which has been constructed but not yet saved from context, based upon a column in database that should be unique. If we have identifier for object already in database, we do not want to resave it. So, I simply delete it from context. This function works as expected, entities are not re-saved in database. The last line do save the objects that are left in context if any. Most of the time there are a lot.
3- Sorry for mistyping AppCalendarEntity and MyEntity are the same.
Solution
I have added this flag fetchRequest.includesPendingChanges = NO; , delete db, restarted Xcode and it started working. Thank you for your persistence