I have to make a complex core data fetch request but I don't know if it can be made.
This is my scenario: just one entity (Expense) with these attributes:
Cost (NSDecimalNumber)
Deposit (NSDecimalNumber)
Category (NSString)
Paid (Boolean Value)
The request should return the 3 most expensive categories but these are the rules that must be respected:
If Paid == YES, Expense cost should be added to Expense category total
If Paid == NO && Deposit > 0, Expense deposit should be added to Expense category total
If Paid == NO, nothing should be added to Expense category total
Using NSExpression, I'm able to calculate every total per category but it also includes cost of Expenses not paid.
Is there a way to accomplish this?
Thank you so much!
You could, for example, use a NSFetchRequest:
// Build the fetch request
NSString *entityName = NSStringFromClass([Expense class]);
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = entity;
which filters only relevant expenses:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(paid == YES) OR ((paid == NO) AND (deposit > 0))"];
request.predicate = predicate;
and sums up the cost and depost attributes:
NSExpressionDescription *(^makeExpressionDescription)(NSString *, NSString *) = ^(NSString *keyPath, NSString *name)
{
// Create an expression for the key path.
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:keyPath];
// Create an expression to represent the function you want to apply
NSExpression *totalExpression = [NSExpression expressionForFunction: #"sum:" arguments: #[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
// The name is the key that will be used in the dictionary for the return value
expressionDescription.name = name;
expressionDescription.expression = totalExpression;
expressionDescription.expressionResultType = NSDecimalAttributeType;
return expressionDescription;
};
NSExpressionDescription *totalCostDescription = makeExpressionDescription(#"cost", #"totalCost");
NSExpressionDescription *totalDepositDescription = makeExpressionDescription(#"deposit", #"totalDeposit");
// Specify that the request should return dictionaries.
request.resultType = NSDictionaryResultType;
request.propertiesToFetch = #[categoryDescription,
paidDescription,
totalCostDescription,
totalDepositDescription];
and group the results by category and paid status:
// Get 'category' and 'paid' attribute descriptions
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName
inManagedObjectContext:context];
NSDictionary *attributes = [entity attributesByName];
NSAttributeDescription *categoryDescription = attributes[#"category"];
NSAttributeDescription *paidDescription = attributes[#"paid"];
// Group by 'category' and 'paid' attributes
request.propertiesToGroupBy = #[categoryDescription, paidDescription];
You'll get paid and unpaid expenses summed up
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
all you need to do is combine (and sort) then:
if (results) {
NSMutableDictionary *combined = [NSMutableDictionary dictionary];
for (NSDictionary *result in results) {
NSString *category = result[#"category"];
BOOL paid = [result[#"paid"] boolValue];
NSDecimalNumber *total = result[paid ? #"totalCost" : #"totalDeposit"];
NSDecimalNumber *sum = combined[category];
if (sum) {
total = [total decimalNumberByAdding:sum];
}
combined[category] = total;
}
NSArray *sortedCategories = [combined keysSortedByValueUsingSelector:#selector(compare:)];
[sortedCategories enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Category %#: %#", obj, combined[obj]);
}];
}
else {
NSLog(#"Error: %#", error);
}
An app I'm building contains a catalogue of thousands of items, which need to be stored on the phone. Currently I am achieving this through CoreData, as logically it seemed like the best place to put it. I'm using GCD to run the CoreData insertion processes in the background and showing a progress bar / current percentage complete. This works as expected, however for only 5000 items, it's taking 8 minutes to complete on an iPhone 4. This application will be used on the 3GS and up, and will more likely contain 30/40 thousand items once it launches. Therefore this processing time is going to be horrifically long.
Is there any way I can use a CSV file or something to search through instead of storing each item in CoreData? I'm assuming there are some efficiency downfalls with an approach like this, but it would alleviate the excessive wait times. Unless there is another solution that would help with this problem.
Thanks.
EDIT:
I'm not sure how I'd go about saving the context at the end of the entire operation, as it uses a separate context within the loop. Any suggestions for this would be very much appreciated. I've got no idea how to progress with this.
Insertion Code Being Used
- (void) processUpdatesGCD {
NSArray *jsonArray=[NSJSONSerialization JSONObjectWithData:_responseData options:0 error:nil];
NSArray *products = [jsonArray valueForKey:#"products"];
NSArray *deletions;
if ([jsonArray valueForKey:#"deletions"] == (id)[NSNull null]){
self.totalCount = [products count];
} else {
deletions = [jsonArray valueForKey:#"deletions"];
self.totalCount = [products count] + [deletions count];
}
self.productDBCount = 0;
_delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = _delegate.managedObjectContext;
self.persistentStoreCoordinator = [managedObjectContext persistentStoreCoordinator];
_managedObjectContext = managedObjectContext;
// Create a new background queue for GCD
dispatch_queue_t backgroundDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
for (id p in products) {
// id product = p;
// Dispatch the following code on our background queue
dispatch_async(backgroundDispatchQueue,
^{
id product = p;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
NSLog(#"Running.. (%#)", product);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [product valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found for add/update (%#)", [product valueForKey:#"product_name"]);
NSManagedObject* newProduct;
newProduct = [NSEntityDescription insertNewObjectForEntityForName:#"Products" inManagedObjectContext:backgroundThreadContext];
[newProduct setValue:[product valueForKey:#"product_name"] forKey:#"name"];
[newProduct setValue:[product valueForKey:#"product_codes"] forKey:#"codes"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[newProduct setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[newProduct setValue:information forKey:#"information"];
}
} else {
NSLog(#"Product found for add/update (%#)", [product valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[r setValue:[product valueForKey:#"product_name"] forKey:#"name"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[r setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[r setValue:information forKey:#"information"];
}
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (add/update). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
// If you have a main thread context you can use it, this time i will create a
// new one
// NSManagedObjectContext *mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// [mainThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Added / updated product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
});
});
}
if ([deletions count] > 0){
for (id d in deletions){
dispatch_async(backgroundDispatchQueue,
^{
id deleted = d;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
// NSLog(#"Running.. (%#)", deleted);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [deleted valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found, can't delete.. %#", [deleted valueForKey:#"product_name"]);
} else {
NSLog(#"Product found, deleting: %#", [deleted valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[backgroundThreadContext deleteObject:r];
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (delete). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Deleted product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
/*
*
* Change the completion changes to a method. Check to see if the total number of products == total count. If it does, run the completion method.
*
*/
});
});
}
}
}
Put the IF inside the dispatch, run one save at the end
// Create a new background queue for GCD
dispatch_queue_t backgroundDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// id product = p;
// Dispatch the following code on our background queue
dispatch_async(backgroundDispatchQueue,
^{
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
for (id p in products) {
id product = p;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
NSLog(#"Running.. (%#)", product);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [product valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found for add/update (%#)", [product valueForKey:#"product_name"]);
NSManagedObject* newProduct;
newProduct = [NSEntityDescription insertNewObjectForEntityForName:#"Products" inManagedObjectContext:backgroundThreadContext];
[newProduct setValue:[product valueForKey:#"product_name"] forKey:#"name"];
[newProduct setValue:[product valueForKey:#"product_codes"] forKey:#"codes"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[newProduct setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[newProduct setValue:information forKey:#"information"];
}
} else {
NSLog(#"Product found for add/update (%#)", [product valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[r setValue:[product valueForKey:#"product_name"] forKey:#"name"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[r setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[r setValue:information forKey:#"information"];
}
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
// If you have a main thread context you can use it, this time i will create a
// new one
// NSManagedObjectContext *mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// [mainThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Added / updated product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
NSDate *currentProcessedDate = [NSDate date];
NSTimeInterval timeSinceStarted = [currentProcessedDate timeIntervalSinceDate:self.startProcessing];
NSInteger remainingProcesses = self.totalCount - self.productDBCount;
float timePerProcess = timeSinceStarted / (float)self.productDBCount;
float remainingTime = timePerProcess * (float)remainingProcesses;
self.timeRemaining.text = [NSString stringWithFormat:#"ETA: %0.0f minutes", fmodf(remainingTime, 60.0f)];
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
/*
*
* Change the completion changes to a method. Check to see if the total number of products == total count. If it does, run the completion method.
*
*/
});
}
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (add/update). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
});
Ok, here's your problem.
Every time you insert a record, you do a save operation to the context.
Now, don't do it, that's what takes alot of time.
Do the save operation once, in the end of the loop, not every time you insert a record.
In your case I would check what is really time consuming?
Is it downloading data, is it importing data to CoreData?
Where do you get data from? Do you download it or you have it in Application Bundle?
CoreData is faster then CSV file. So it wont make your app faster.
Some tricks:
While importing data just save context at the end of the process. Do not save context in a loop.
If you do not need to download data and can put in the bundle, you can create coredata file in the simulator, put in the bundle and copy the file on first launch. It is really much more faster then importing data.
I have this piece of code which I am using to update some values in Core Data when another object is added:
//Create new receipt
Receipt *receipt = [[Receipt alloc] init];
receipt.project = self.projectLabel.text;
receipt.amount = self.amountTextField.text;
receipt.descriptionNote = self.descriptionTextField.text;
receipt.business = self.businessNameTextField.text;
receipt.date = self.dateLabel.text;
receipt.category = self.categoryLabel.text;
receipt.paidBy = self.paidByLabel.text;
receipt.receiptImage1 = self.receiptImage1;
//Need to set this to 2
receipt.receiptImage2 = self.receiptImage1;
receipt.receiptNumber = #"99";
int count = 0;
int catCount = 0;
for (Project *p in appDelegate.projects)
{
if ([p.projectName isEqualToString:receipt.project]){
double tempValue = [p.totalValue doubleValue];
tempValue += [receipt.amount doubleValue];
NSString *newTotalValue = [NSString stringWithFormat:#"%.02f", tempValue];
NSString *newProjectName = p.projectName;
//remove entity from Core Data
NSFetchRequest * allProjects = [[NSFetchRequest alloc] init];
[allProjects setEntity:[NSEntityDescription entityForName:#"Project" inManagedObjectContext:appDelegate.managedObjectContext]];
[allProjects setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * error = nil;
NSArray * projectsArray = [appDelegate.managedObjectContext executeFetchRequest:allProjects error:&error];
//Delete product from Core Data
[appDelegate.managedObjectContext deleteObject:[projectsArray objectAtIndex:count]];
NSError *saveError = nil;
[appDelegate.managedObjectContext save:&saveError];
[appDelegate.projects removeObjectAtIndex:count];
NSLog(#"Removed project from Core Data");
//Insert a new object of type ProductInfo into Core Data
NSManagedObject *projectInfo = [NSEntityDescription
insertNewObjectForEntityForName:#"Project"
inManagedObjectContext:appDelegate.managedObjectContext];
//Set receipt entities values
[projectInfo setValue:newProjectName forKey:#"name"];
[projectInfo setValue:newTotalValue forKey:#"totalValue"];
if (![appDelegate.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Added Project to Core Data");
Project *tempProject = [[Project alloc] init];
tempProject.projectName = [projectInfo valueForKey:#"name"];
tempProject.totalValue = [projectInfo valueForKey:#"totalValue"];
[appDelegate.projects addObject:tempProject];
}
count++;
}
for (Category *c in appDelegate.categories){
if ([c.categoryName isEqualToString:receipt.category]){
double tempValue = [c.totalValue doubleValue];
tempValue += [receipt.amount doubleValue];
NSString *newTotalValue = [NSString stringWithFormat:#"%.02f", tempValue];
NSString *newCategoryName = c.categoryName;
//remove entity from Core Data
NSFetchRequest * allCategories = [[NSFetchRequest alloc] init];
[allCategories setEntity:[NSEntityDescription entityForName:#"Category" inManagedObjectContext:appDelegate.managedObjectContext]];
[allCategories setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * categoriesError = nil;
NSArray * categoriesArray = [appDelegate.managedObjectContext executeFetchRequest:allCategories error:&categoriesError];
//Delete product from Core Data
[appDelegate.managedObjectContext deleteObject:[categoriesArray objectAtIndex:catCount]];
NSError *categorySaveError = nil;
[appDelegate.managedObjectContext save:&categorySaveError];
[appDelegate.categories removeObjectAtIndex:catCount];
NSLog(#"Removed category from Core Data");
NSError * error = nil;
//Insert a new object of type ProductInfo into Core Data
NSManagedObject *categoryInfo = [NSEntityDescription
insertNewObjectForEntityForName:#"Category"
inManagedObjectContext:appDelegate.managedObjectContext];
//Set receipt entities values
[categoryInfo setValue:newCategoryName forKey:#"name"];
[categoryInfo setValue:newTotalValue forKey:#"totalValue"];
if (![appDelegate.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Added Category to Core Data");
Category *tempCategory = [[Category alloc] init];
tempCategory.categoryName = [categoryInfo valueForKey:#"name"];
tempCategory.totalValue = [categoryInfo valueForKey:#"totalValue"];
[appDelegate.categories addObject:tempCategory];
}
catCount++;
}
This code gives the error:
'...was mutated while being enumerated'.
Can anyone explain why? Also, is there a better approach to doing what I'm trying to achieve?
The error you're seeing is accurate. The problem is that you're mutating (changing) a collection while iterating over it. Basically, you're doing something of the form:
for (Project *p in appDelegate.projects) {
...
[p addObject: X]
}
This isn't allowed.
One simple solution is to make a new collection of the objects you want to add, and then add them to the original container outside of your loop. Something like:
NSMutableArray *array = [NSMutableArray array];
for (Project *p in appDelegate.projects) {
...
[array addObject:X];
}
[p addObjects:array];
By the way, did you google for the error text "was mutated while being enumerated"? I'd be surprised if you didn't find the answer for this common problem just by googling.
Also, when posting an error message, it's helpful to post the full line, not just part of it.
You are adding and removing items from appDelegate.projects while iterating over it in a for-each loop here:
[appDelegate.projects removeObjectAtIndex:count];
// ...
[appDelegate.projects addObject:tempProject];
And the same for appDelegate.categories:
[appDelegate.categories removeObjectAtIndex:count];
// ...
[appDelegate.categories addObject:tempProject];
AFAIK, in such case you should use a simple for loop, and access the array with index.
I have a method which is void but I have to count the number of entries in it.
So is there any way I can do it??
I tried to implement it but its not working.
It's giving me a error at Nsfetch count.
The return is declared as null.
testcase1
{
//startdate and end date are same and both are 1 day before
NSTimeInterval secondsPerday=24*60*60;
NSDate *startdate = [[NSDate alloc]initWithTimeIntervalSinceNow:-secondsPerday];
NSDate *endate = [[NSDate alloc]initWithTimeIntervalSinceNow:-secondsPerday];
TravelerAppDelegate *delegate =[[UIApplication sharedApplication]delegate];
[delegate getWeatherforCity:#"#" state:#"#" country:#"#" startDate:startdate endDate:endate];
NSManagedObjectContext *allone=[delegate managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Weather" inManagedObjectContext:allone];
//WeatherXMLParser *delegate = [[WeatherXMLParser alloc] initWithCity:#"#" state:#"#" country:#"#"];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Weather" inManagedObjectContext:allone]];
[request setIncludesSubentities:NO];
NSError *err;
NSUInteger count = [allone countForFetchRequest:request error:&err];
if(count == NSNotFound) {
//Handle error
}
[request release];
return count;
}
First, you define the variable entity but when you set the entity of the fetch request, you are doing the same as above. Simply write:
[request setEntity:entity];
Then, NSFetchRequest has to have a sort descriptor array. So, add this:
[request setSortDescriptors:[NSArray array]];
If you want a method to return something, then set its return type.
-(void)testcase1
{
// Code goes here
}
Nothing (void) is returned. You'd call a method like this by:
[self testcase1];
To return something:
-(NSInteger)testcase1
{
// Code goes here
return count;
}
You would call such a method like this:
NSInteger count = [self testcase1];
I've included the function that is handling all of the save functionality.
Here's my problem.
I am grabbing 5 input values and saving it as a CoreData Log Entity.
Even when the Log object fails to validate, it is still being saved when I back out of the form and look at the table view.
How can I force Core Data to only save the object once it's validated?
-(void) saveLog {
NSManagedObjectContext *managedObjectContext = [(AppDelegate_Shared *)[[UIApplication sharedApplication] delegate] managedObjectContext];
FormPickerCell *bloodPressure = (FormPickerCell *) [self.formController fieldAsObject:#"bloodpressure"];
NSInteger systolic = [(PressureDataSource*)bloodPressure.pickerCellDelegate selectedSystolicPressureForFormPickerCell:bloodPressure];
NSInteger diastolic = [(PressureDataSource*)bloodPressure.pickerCellDelegate selectedDiastolicPressureForFormPickerCell:bloodPressure];
NSLog(#"bp is %d / %d", systolic, diastolic);
NSLog(#"date is %#", [self.formController valueForField:#"date"]);
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd HH:mm:ss ZZZ"];
if (self.isNewLog && !self.validationHasFailed) {
self.log = [NSEntityDescription
insertNewObjectForEntityForName:#"Log" inManagedObjectContext:managedObjectContext];
}
NSString *heartRate = [[self.formController valueForField:#"heartrate"] stringByReplacingOccurrencesOfString:#" bpm" withString:#""];
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterDecimalStyle];
self.log.created = [NSDate date];
self.log.notes = [self.formController valueForField:#"notes"];
self.log.systolic = [NSNumber numberWithInteger:systolic];
self.log.diastolic = [NSNumber numberWithInteger:diastolic];
self.log.stressLevel = [self.formController valueForField:#"stresslevel"];
self.log.logDate = [dateFormatter dateFromString:[self.formController valueForField:#"date"]];
self.log.heartrate = [f numberFromString:heartRate];
NSLog(#"Log date is %#",[self.formController valueForField:#"date"]);
[f release];
NSError *error;
NSString *title;
NSString *growlDescription;
if ([self.log validateForInsert:&error]){
NSLog(#"after validation returned true");
if(![managedObjectContext save:&error]) {
NSLog(#"Unresolved error");
title = #"Error Occurred";
growlDescription = [error localizedDescription];
self.validationHasFailed = YES;
} else {
title = #"Log Saved!";
growlDescription = #"Log saved successfully";
[self.navigationController popViewControllerAnimated:YES];
}
} else {
NSLog(#"after validation returned false");
NSLog(#"Unresolved error");
title = #"Error Occurred";
growlDescription = [error localizedDescription];
self.validationHasFailed = YES;
}
IZGrowlNotification *notification = [[IZGrowlNotification alloc] initWithTitle:title
description:growlDescription
image:nil
context:nil
delegate:self];
[[IZGrowlManager sharedManager] postNotification:notification];
[notification release];
error = nil;
}
This is a bit late but I just saw your question so figured I'd toss an answer at you. Any object you add to the managed object context will be saved whenever you next save. You could leave your code as is and just delete the new object with [managedObjectContext deleteObject:self.log] but a better method is below.
Your code:
self.log = [NSEntityDescription insertNewObjectForEntityForName:#"Log" inManagedObjectContext:managedObjectContext];
Creates a new Log instance and inserts into the managed object context. What you want to do instead is:
self.log = [[Log alloc] initWithEntity:[NSEntityDescription entityForName:#"Log" inManagedObjectContext:managedObjectContext] insertIntoManagedObjectContext:nil];
This will create a new 'Log' instance that has not yet been inserted into the MOC. If validation succeeds, before you save the MOC you insert the self.log as follows:
[managedObjectContext insertObject:self.log];
Then you save. If validation fails, don't insert the object and you're good to go.