Core Data Edit/Save Attributes in an Entity - objective-c

I am struggling with the editing/saving in Core Data and need some help in this. I am using NSFetchedResultsController and have an entity named Golfer with attributes- first_name, last_name, email_id and others in Core Data. So, I know how to add and remove golfers from the database.
I am working on one view controller called ViewManager (kinda base view for all my classes) and it has 2-3 Custom UIViews inside it. I animate them in and out whenever I need them.
I add a golfer to the tableview, then on didSelectRow tableview method, I present my edit View inside the same ViewManager controller and try to update the textfields in the edit view using the following code, but it's updating at random indexes in the tableview and not working for me. Any help would be greatly appreciated.
- (IBAction)saveEditGolfersView:(id)sender
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
// Retrieve the entity from the local store -- much like a table in a database
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Golfer" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
// Set the sorting -- mandatory, even if you're fetching a single record/object
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"first_name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release]; sortDescriptors = nil;
[sortDescriptor1 release]; sortDescriptor1 = nil;
NSError * error;
NSArray * objects = [context executeFetchRequest:request error:&error];
for(int i = 0; i<[objects count]; i++)
{
Golfer * golfguy = [objects objectAtIndex:i];
golfguy.first_name = mEditFirstName.text;
golfguy.middle_name = mEditMiddleName.text;
golfguy.last_name = mEditLastName.text;
golfguy.email_id = mEditEmailField.text;
golfguy.contactNumber = mEditContactNum.text;
golfguy.picture = mEditPictureView.image;
NSLog(#"name-%#", golfguy.first_name);
}
[request release]; request = nil;
error = nil;
[context save:&error];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut
animations:^ {
mEditGolfersView.frame = CGRectMake(-480, mEditGolfersView.frame.origin.y, mEditGolfersView.frame.size.width, mEditGolfersView.frame.size.height);
}
completion:^(BOOL finished) {
mEditGolfersView.hidden = YES;
}];
}

If I have read this code correct, then a call to -(IBAction)saveEditGolfersView:(id)sender will set all the Golfers with the exact properties, which I expect is not what you want.
I am not quite sure what the problem is, but I hypothesize that you need an NSPredicate to go along with your NSFetchRequest in order to change the correct Golfer(s).
Maybe I missed something, but this code says to me "hey, I'm going to load all of the Golfer in the database, order them by their first name, and then set all of their properties to the exact same text fields on this page". Just sounds like bad news...
To edit just one golfer, be sure to store the golfer you are editing in a property some where. Since you keep the managedObjectContext stored on the applicationDelegate, it will stay alive and thus keep your core data objects alive. That would avoid the expensive fetch that you are doing in the save view. If, however, you do not want to keep a reference to the golfer object, each NSManagedObject has an objectId, which is the identifier used by core data. You could use the objectId in a fetch predicate like so:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"objectId == %#", self.editingGolferObjectId];
[request setPredicate:predicate];
I would choose to keep reference to the object in your case, rather than the objectId

Related

Core Data not updating, updated model

I have a very weird problem that has stumped me rather!
I have a core data entity that i have just added some new attributes to:
deleted - Boolean
deletedDate - Date
I have the following code, that upon pressing sets both those values on the core data object:
- (IBAction)deleteButtonInTable:(id)sender {
//Get the ID of the currently selected item in the table
NSInteger selected = [self.tweetTableView rowForView:sender];
//Create a predicate and fetch the objects from Core Data
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *testForTrue = [NSPredicate predicateWithFormat:#"approved == NO"];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"postDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[request setPredicate:testForTrue];
[request setSortDescriptors:sortDescriptors];
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
//Setup the Request
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
//Assign the predicate to the fetch request
NSError *error = nil;
//Create an array from the returned objects
NSArray *fetchedObjects = [_managedObjectContext executeFetchRequest:request error:&error];
Tweet *selectedTweet = [fetchedObjects objectAtIndex:selected];
if (selectedTweet) {
selectedTweet.deleted = [NSNumber numberWithBool:TRUE];
selectedTweet.deletedDate = [NSDate date];
NSLog(#"%#",selectedTweet);
[self refreshTableView];
if (! self.tweetTableView){
NSLog(#"Tableview doesn't exist!!)");
}
[[self tweetTableView] reloadData];
[[self managedObjectContext] commitEditing];
[self saveAction:nil];
}
if ([self.autoWriteTweets isEqualToString:#"YES"]){
[self writeTweetsToXML];
[self saveAction:nil];
}
}
Now, if i watch the object in xcode with some breaks, i can see the attribute change on the object as i pass through the function, but i have an Table displaying a datasource, which is filtered to only show objects that have the deleted bool set to true, and nothing ever shows up there.
Now, to make things even more confusing i have a function that exports an array of the objects:
-(void)writeTweetsToXML{
//Create new fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
//Set new predicate to only fetch tweets that have been favourited
NSPredicate *filterFavourite = [NSPredicate predicateWithFormat:#"approved == YES"];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:self.exportSort ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[request setSortDescriptors:sortDescriptors];
//Setup the Request
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
[request setResultType:NSDictionaryResultType];
//Assign the predicate to the fetch request
[request setPredicate:filterFavourite];
NSError *error = nil;
//Create an array from the returned objects
NSArray *tweetsToExport = [_managedObjectContext executeFetchRequest:request error:&error];
NSAssert2(tweetsToExport != nil && error == nil, #"Error fetching events: %#\n%#", [error localizedDescription], [error userInfo]);
//NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//NSString *path = [NSString stringWithFormat:#"%#/tweets.xml", documents];
NSString *writeerror;
if(tweetsToExport) {
NSString * exportLocationFull = [[NSString alloc]initWithFormat:#"%#/tweets.xml",self.exportLocation];
BOOL success = [tweetsToExport writeToFile:exportLocationFull atomically:YES];
NSLog(#"Write Status = %d to %#", success, exportLocationFull);
}
else {
NSLog(#"%#",writeerror);
}
}
Now, when i look at the exported file, two things happen which are odd!
Firstly, an object that i have seen have it's deleted value set to true, exports with the value as 0.
Secondly, the deletedDate attribute does not export at all, every, despite it being in the core data model. I can't see any way this can happen as i am doing no specific filtering on the export.
It's like a getter/setter somewhere is broken, but i have checked the class files and everything is as it should be and set to #dynamic.
Any help would be greatly appreciated as i'm a bit lost as to what the hell is going on.
People had warned me about core data's quirks, but this is just plain odd!
Cheers
Gareth
Note 1
As an aside, i am using the exact same code from the first section to set other attributes on objects that are filtered and that seems to work fine!
You should not name an Core Data attribute "deleted", that conflicts with the
isDeleted method of NSManagedObject.
Compare https://stackoverflow.com/a/16003894/1187415 for a short analysis of that problem.
There are other attribute names that cause conflicts, e.g. "updated" (compare Cannot use a predicate that compares dates in Magical Record). Unfortunately, there are no warnings at compile time or runtime,
and the documentation on what acceptable attribute names are is also quite vague.
Things to check:
Did you save your core data entities with [managedObjectContext save:&error] at the appropriate places (e.g. before displaying the new table view data)? Did you check the error variable?
Did you migrate your model correctly with a new model version?
Are you reading the correct attributes and displaying them correctly (in UI or log statements)?
BTW, in your code you are setting the request entity twice.
Try saving the mananged object context before loading the table view.
The boolean deleted may be 0 before and not be changed or it may be auto-initialized (there is an field in the inspector to set default values) to 0. Date fields on the other hand are nil by default.
P.S. Use [NSNumber numberWithBoolean:YES] in Objective-C.

Entity Inheritance - Core Data iOS

Describing Core Data Model:
I have a Core data model for my Expense tracking application. I made an abstract parent entity named "Money" with attributes "vendor", "date", "amount" and so on. The two sub-entities "Expense" and "Income" inherit from the parent entity "Money". Then there are other entities such as "Category" and "subCategory" with their attributes. Total as of now: 5 entities in my data model.
I have a relationship from the sub-entities "Expense" and "Income" to entity "Category" which in turn has a to-many relationship with the entity "SubCategory" as a category can have one or many sub-categories. The entity "Category" has a to-many relationship to the entities "Expense" and "Income" as well as there can be may expenses or incomes for a particular category. That makes sense to me as of now.
I have a Table-view and using NSFetchedResultsController to fill my table-view with the mix od expenses and Incomes.
I have an alert view with buttons "ExpenseS" and "Income" - both are pushed to a same view controller which has the details like fill a vendor, amount, category, sub-category and SAVE button on the navigation bar.
That's my save method:
- (void)saveMoney:(id)sender
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
_money = (Money*) [NSEntityDescription insertNewObjectForEntityForName:#"Money" inManagedObjectContext:context];
double expenseAmt = [_amountTxtField.text doubleValue];
NSDate *expenseDate = [dateFormatter dateFromString:[NSString stringWithFormat:#"%#",currentDateLbl.text]];
_money.vendor = vendorTxtField.text;
_money.cat = categoryLbl.text;
_money.subcat = subCategoryLbl.text;
_money.amount = [NSNumber numberWithDouble:expenseAmt];
_money.date = expenseDate;
_money.photo = _templateImgView.templateImage.image;
_money.notes = notesLbl.text;
_money.paidBy = paidResourceLbl.text;
NSError * error = nil;
[context save:&error];
[self.navigationController dismissModalViewControllerAnimated:YES];
}
This saves the new expense/income to the table-view and I get a mix of the results(expenses/income) in my table view. But how will I know if my row is an expense or an income. I think a fetch request should make it work? But where this fetch request should be placed, in what method? I tried using the following code:
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
// Retrieve the entity from the local store -- much like a table in a database
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Money" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
[request setIncludesSubentities:YES];
// Set the sorting -- mandatory, even if you're fetching a single record/object
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release]; sortDescriptors = nil;
[sortDescriptor1 release]; sortDescriptor1 = nil;
NSError * error;
NSArray * objects = [context executeFetchRequest:request error:&error];
for (Money* money in objects)
{
NSLog(#"money class");
if([money isKindOfClass:[Expense class]])
{
NSLog(#"expense class");
}
else
{
}
}
[context save:&error];
This never prints the "expense class", doesn't go there at all. I don't know how this shall work and in what method I shall place this fetch request.
Please help me out here. I will appreciate it.
Thank you
When you create your entities you should use Expense or Income as the entity name, e.g.:
[NSEntityDescription insertNewObjectForEntityForName:#"Expense" inManagedObjectContext:context];
Then when you fetch using the entity name 'Money' it'll pull out the sub-entities as you're expecting.

UItableview not refreshing updated data from core data

i have a uitableview with a method fetch list that runs every 5 secs. when there is updated data in the core data for that record, the fetch list method does not update the latest into its array. thus, when i reload the data, it always shows the "old" record.
i call this method, followed by a reload data on the uitableview.
this is my fetch list method:
- (void) fetchList:(int) listNo{
// Define our table/entity to use
self.marketWatchListArray = nil;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Market_watch" inManagedObjectContext:context];
// Setup the fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
Mobile_TradingAppDelegate *appDelegate = (Mobile_TradingAppDelegate *)[[UIApplication sharedApplication] delegate];
NSPredicate *getList = [NSPredicate predicateWithFormat:#"(list_id == %d) AND (userID == %#)", listNo, appDelegate.user_number];
[request setPredicate:getList];
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:#"stockName" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sortByName]];
// Fetch the records and handle an error
NSError *error;
NSMutableArray *mutableFetchResults = [[context executeFetchRequest:request error:&error] mutableCopy];
if (!mutableFetchResults) {
// Handle the error.
// This is a serious error and should advise the user to restart the application
}
// Save our fetched data to an array
[self setMarketWatchListArray: mutableFetchResults];
[mutableFetchResults release];
[request release];
}
strange thing is the table does not update with the latest when the timer runs fetchlist:1. When i swap to fetchlist:2, then back to fetchlist:1, the table is updated with the latest.
I have a segmentcontrol to toggle between different list.
Add this line before the fetch:
[context setStalenessInterval: 4.0]; // allow objects to be stale for max of 4 seconds
At the end of fetchList method, you add below code to refresh data in UITableView :
[yourTableView reloadData];

Adding unique objects to Core Data

I'm working on an iPhone app that gets a number of objects from a database. I'd like to store these using Core Data, but I'm having problems with my relationships.
A Detail contains any number of POIs (points of interest). When I fetch a set of POI's from the server, they contain a detail ID. In order to associate the POI with the Detail (by ID), my process is as follows:
Query the ManagedObjectContext for the detailID.
If that detail exists, add the poi to it.
If it doesn't, create the detail (it has other properties that will be populated lazily).
The problem with this is performance. Performing constant queries to Core Data is slow, to the point where adding a list of 150 POI's takes a minute thanks to the multiple relationships involved.
In my old model, before Core Data (various NSDictionary cache objects) this process was super fast (look up a key in a dictionary, then create it if it doesn't exist)
I have more relationships than just this one, but pretty much every one has to do this check (some are many to many, and they have a real problem).
Does anyone have any suggestions for how I can help this? I could perform fewer queries (by searching for a number of different ID's), but I'm not sure how much this will help.
Some code:
POI *poi = [NSEntityDescription
insertNewObjectForEntityForName:#"POI"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
poi.POIid = [attributeDict objectForKey:kAttributeID];
poi.detailId = [attributeDict objectForKey:kAttributeDetailID];
Detail *detail = [self findDetailForID:poi.POIid];
if(detail == nil)
{
detail = [NSEntityDescription
insertNewObjectForEntityForName:#"Detail"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
detail.title = poi.POIid;
detail.subtitle = #"";
detail.detailType = [attributeDict objectForKey:kAttributeType];
}
-(Detail*)findDetailForID:(NSString*)detailID {
NSManagedObjectContext *moc = [[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Detail" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"detailid == %#", detailID];
[request setPredicate:predicate];
NSLog(#"%#", [predicate description]);
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil || [array count] != 1)
{
// Deal with error...
return nil;
}
return [array objectAtIndex:0];
}
Check out the section titled "Batch Faulting" on the page titled "Core Data Performance" in Xcode's Core Data Programming Guide that Norman linked to in his answer.
Only fetching those managedObjects whose ids are IN a collection (NSSet, NSArray, NSDictionary) of ids of the objects returned by the server may be even more efficient.
NSSet *oids = [[NSSet alloc] initWithObjects:#"oid1", #"oid2", ..., nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"oid IN %#", oids];
[oids release];
UPDATE: I worked this tip into a solution for the acani usersView. Basically, after downloading a JSON response of users, the iPhone uses the popular open source JSON framework to parse the response into an NSArray of NSDictionary objects, each representing a user. Then, it makes an NSArray of their uids and does a batch fetch on Core Data to see if any of them already exist on the iPhone. If not, it inserts it. If so, it updates the ones that do exist only if their updated attribute is older than that of the one from the server.
I've gotten all this to work really well, thanks to Norman, who put me on the right path. I'll post my helper class here for others.
Basically, my helper class will look up if an NSManagedObject exists for some ID, and can create it for some ID. This executes quickly enough for me, with 1,000 find/create operations taking around 2 seconds on my iPhone (I also did a few other things there, pure find/create is likely faster).
It does this by caching a dictionary of all the NSManagedObjects, and checking that cache rather than executing a new NSFetchRequest.
A couple of modifications that could help things speed up even further:
1. Get only selected properties for the NSManagedObjects
2. Only get the identifier property for the NSManagedObject into a dictionary, instead of the whole object.
In my performance testing, the single query wasn't the slow part (but with only 1,000 items, I'd expect it to be fast). The slow part was the creation of the items.
#import "CoreDataUniquer.h"
#implementation CoreDataUniquer
//the identifying property is the field on the NSManagedObject that will be used to look up our custom identifier
-(id)initWithEntityName:(NSString*)newEntityName andIdentifyingProperty:(NSString*)newIdProp
{
self = [super init];
if (self != nil) {
entityName = [newEntityName retain];
identifyingProperty = [newIdProp retain];
}
return self;
}
-(NSManagedObject*)findObjectForID:(NSString*)identifier
{
if(identifier == nil)
{
return nil;
}
if(!objectList)
{
NSManagedObjectContext *moc = [(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:entityName inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
objectList = [[NSMutableDictionary dictionary] retain];
for (NSManagedObject* p in array) {
NSString* itemId = [p valueForKey:identifyingProperty];
[objectList setObject:p forKey:itemId];
}
}
NSManagedObject* returnedObject = [objectList objectForKey:identifier];
return returnedObject;
}
-(NSManagedObject*)createObjectForID:(NSString*)identifier
{
NSManagedObject* returnedObject = [NSEntityDescription
insertNewObjectForEntityForName:entityName
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
[returnedObject setValue:identifier forKey:identifyingProperty];
[objectList setObject:returnedObject forKey:identifier];
return returnedObject;
}
- (void) dealloc
{
DESTROY(entityName);
DESTROY(identifyingProperty);
[super dealloc];
}
#end
This page provides some help on optimizing performance:
http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdPerformance.html#//apple_ref/doc/uid/TP40003468-SW1
While not very efficient, why not just build them in-memory with a NSDictionary? Read everything from Core Data into a NSDictionary then merge in your data, replacing everything in Core Data.

"Save" in CoreData-apps

I've here a CoreData app (no document-based), 1 entity and 1 tableview for edit/add/remove "instances" of the entity. Now I can manual add and save but I would like to
a) save automaticly changes
b) add automaticly some "instances" with the first start.
I think a) can be solved with NSNotifications. But which to use by entities?
Any ideas how to solve a) or b)?
Thanks for every answer. =)
Autosave can be a bit trickier than you'd first expect sometimes, since there may be times when your application data is in an invalid state (for instance, when the user is editing an entity) and either cannot be saved or it would not make sense to save. There's no easy setAutosaves:YES property unfortunately, so you'll have to implement it yourself. Using a notification to save after certain actions is one way to do it, you could also set up a timer to save periodically if it makes sense for your application.
To populate an empty data file, just check to see if the data store is empty at launch (applicationDidFinishLaunching and awakeFromNib are two possible places to put this), and if it is insert some entities as normal. The only tricky part is disabling undo management during the process. Here's an example from one of my applications:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
{
NSURL *fileURL = [NSURL fileURLWithPath:[self.applicationSupportFolder stringByAppendingPathComponent:WLDataFileName]];
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:fileURL options:nil error:NULL];
[context setPersistentStoreCoordinator:coordinator];
[request setEntity:[NSEntityDescription entityForName:#"Shelf" inManagedObjectContext:context]];
if ( [context countForFetchRequest:request error:NULL] == 0 )
[self _populateEmptyDataStore:context];
_managedObjectContext = [context retain];
[request release];
[coordinator release];
[context release];
// finish loading UI, etc...
}
- (void)_populateEmptyDataStore:(NSManagedObjectContext *)context;
{
[[context undoManager] disableUndoRegistration];
WLSmartShelfEntity *allItems = [NSEntityDescription insertNewObjectForEntityForName:#"SmartShelf" inManagedObjectContext:context];
WLSmartShelfEntity *trash = [NSEntityDescription insertNewObjectForEntityForName:#"SmartShelf" inManagedObjectContext:context];
allItems.name = NSLocalizedString( #"All Items", #"" );
allItems.predicate = [NSPredicate predicateWithFormat:#"isTrash = FALSE"];
allItems.sortOrder = [NSNumber numberWithInteger:0];
allItems.editable = [NSNumber numberWithBool:NO];
trash.name = NSLocalizedString( #"Trash", #"" );
trash.predicate = [NSPredicate predicateWithFormat:#"isTrash = TRUE"];
trash.sortOrder = [NSNumber numberWithInteger:2];
trash.editable = [NSNumber numberWithBool:NO];
[context processPendingChanges];
[[context undoManager] enableUndoRegistration];
DebugLog( #"Filled empty data store with initial values." );
}
Have a look at this thread on the Apple Mailing lists regarding autosave and Core Data.