cocoa, table view with core data datasource - objective-c

I am absolutely new to cocoa development so please don't chastise me for asking this.
Do I have to uses bindings? I know it saves a lot of code, but since I'm learning I'd really prefer to set everything up in the code.
My goal is to populate a table view with the entities I fetch from core data. I've created a custom controller class for my main window view. I also have some code to fetch my category entites
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Category"
inManagedObjectContext:[self managedObjectContext]];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjectsArray = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error];
if (fetchedObjectsArray == nil) {
NSLog(#"Error occured fetching categories: %#", error);
}
for (Category *cat in fetchedObjectsArray) {
NSLog(#"categories %#", cat.name);
}
I know I have to implement the delegates for my table view, but I'm kinda lost on where to put my fetch code so the table view 1. has access to it and 2. uses the data returned from my fetch request.

You put the fetch code at that location of your program at which fetching is useful.
You store the data in a way that is convenient for you, probably a property typed array in your controller.
After fetching the data, you tell the table view, that there is new data to display by sending a reloadData (et al.) message to it.
The data source of the table view, probably your controller will receive messages defined within the data source protocol to transport the date into the view. Since this is code written by you, too, it is totally under your controller, how you get the data. (See step 2).

Related

NSManagedContext issue fetching from context multiple times

I have two view controllers. One a searchResults tableview controller (VC1) where the user see a list of rows matching a selection criteria and other ViewDetail (tableview controller) (VC2) where the user sees Detail for the chosen row of VC1. The info required to fetch detail for the chosen row along with the managedContext reference are passed from VC1 to VC2 in the prepareforsegue method of VC1 by setting the relevant properties of VC2. During my test, I switched between (using the navigation controller back button) VC1 and VC2 each time selecting a different row on VC1 to see the detail of a different item. This works normally for 7-15 times of switching but crashes suddenly after some attempts of switching. I have investigated this as far as I could but stuck without a solution and hence posting this. Please help. The error is that a particular Array is out of bounds for index 0. While I understand however, I do not expect this array which is populated by results of a fetch request to be empty. Hence I suspect that there is something wrong with the managedcontext. Snippet of code from VC2 is provided
//All this is in ViewDidLoad of VC2 App crashes at the last line of this snippet. trying to get an object at index 0 which is non existent but should not be ...
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#" cameraid == %#",(NSNumber *)self.selectedCameraid];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Cameras"
inManagedObjectContext:self.managedContext];
//Configure core data request
[request setEntity:entity];
[request setPredicate:predicate];
//Execute request
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[self.managedContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
NSLog(#"Some error in fetching results");
}
NSLog(#"Mutable fetch results data %#",mutableFetchResults);
self.resultsArray = mutableFetchResults;
Cameras *rowdata = [self.resultsArray objectAtIndex:0]; //Cameras is a managed object
After a bit of further digging I found the issue... I am type casting a string as NSNumber (NSNumber *)self.selectedCameraid in the above code. Apparently this is what has been causing the crash. I converted the NSString object to a NSNumber object using NSNumberformatter and everything seems to work fine. I realised it is not safe to type cast objects in this manner.

Updating core data entry

I'm working with core data to save scores for a game, and need help updating an entry when a score is beaten. This is where I'm at. I write the data the first time the app launches like this:
LevelData *levelOne = [NSEntityDescription insertNewObjectForEntityForName:#"LevelData" inManagedObjectContext:context];
levelOne.levelNum = #"1";
levelOne.topScore = #"0";
levelOne.isPassed = #"No";
if (![context save:&error]) {
NSLog(#"coudlnt save: %#", [error localizedDescription]);
}
I then read out the data at the end of the level like this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"levelNum == 1"];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"LevelData" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (LevelData *info in fetchedObjects) {
NSLog(#"Is Passed: %#", info.levelNum);
NSLog(#"Top Score: %#", info.topScore);
NSLog(#"Is Passed: %#", info.isPassed);
}
What I'm stuck doing is updating the topScore entry and writing it back to the data store without creating a new entry, but updating the existing one.
And help/example would be so so helpful and much appreciated.
Thanks, Kyle
Well, updating the entry from the data store is easy. Fetch, update, save. As you seem to have a handle on. The real issue with core data is defining that initial data store. The design of core data does not bring in the concept of a database, so typically, one does not exist until the backend implements it for you (after your application calls its first save).
What I have seen developers typically do to work around this is set up AppDelegate to implement a quick and dirty hack: save an empty data base. They then comment out this code; retrieve the file from disk, rename it, modify it, add it to the project's support files.
Now, revisiting AppDelegate, they test on application launch the existence of the datastore on the client's machine. If none exists, they know to inject the default data store from their support files.. basically copying it to the appropriate path and renaming it appropriately as well.
This gets overly complicated with versioning. Another approach to this is to write a plist of the defaults into the main bundle and import this on the first launch and store one key in user defaults to let me know if this is necessary. It requires a little more boilerplate code, but it is a great way to get things set up. And you can encode versioning data to support upgrade merging a lot easier. If you can handle core data well, manipulating XML shouldn't be much more difficult.

Not getting data from Core Data

I am using Core Data to store some information for my app.
I have a .xcdatamodeld file containing 8 entities, and I extract them on different views.
In one of the viewControllers, I call three of them. Like this:
AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication]delegate];
managedObjectContext = appDelegate.managedObjectContext;
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entiAll = [NSEntityDescription entityForName:#"AllWeapons" inManagedObjectContext:moc];
NSFetchRequest *frAll = [[NSFetchRequest alloc] init];
[frAll setEntity:entiAll];
NSError *error = nil;
arrAll = [moc executeFetchRequest:frAll error:&error];
displayArray = [[NSMutableArray alloc]initWithArray:arrAll];
NSEntityDescription *entiRange = [NSEntityDescription entityForName:#"WeaponsRanged" inManagedObjectContext:moc];
NSFetchRequest *frRanged = [[NSFetchRequest alloc] init];
[frRanged setEntity:entiRange];
NSError *errorRanged = nil;
arrRange = [moc executeFetchRequest:frRanged error:&errorRanged];
NSLog(#"%i, %i", [arrRange count], [[moc executeFetchRequest:frRanged error:&errorRanged] count]);
NSEntityDescription *entiMelee = [NSEntityDescription entityForName:#"WeaponsMelee" inManagedObjectContext:moc];
NSFetchRequest *frMelee = [[NSFetchRequest alloc] init];
[frMelee setEntity:entiMelee];
NSError *errorMelee = nil;
arrMelee = [moc executeFetchRequest:frMelee error:&errorMelee];
NSLog(#"%i, %i", [arrMelee count], [[moc executeFetchRequest:frMelee error:&errorMelee] count]);
The problem is that the middle one (the one filling the arrRange-array) doesn't work..
arrAll logs out with all correct data, arrMelee logs out with all the correct data (x4 for some reason, don't know if this is related :S), but arrRange logs out as an empty array.
[arrRange count]; gives me 0, even though I know there is lots of data there.
I ran this code on the simulator, and found the .sqlite file, opened it in Firefox's SQLite Manager, and saw the correct data, 40 rows.
I went into the appDelegate, where I fill the CoreData when necessary, and saw that the method which downloads the data in JSON-format successfully sends it to the sqlite aswell.
Here I fill the CoreData with data from the json:
[self deleteAllObjects:#"WeaponsRanged"];
NSManagedObjectContext *context = [self managedObjectContext];
for(NSDictionary *item in jsonWeaponRanged)
{
WeaponsRanged *wr = [NSEntityDescription insertNewObjectForEntityForName:#"WeaponsRanged"
inManagedObjectContext:context];
///***///
wr.recoil = [item objectForKey:#"Recoil"];
///***///
NSError *error;
if(![context save:&error])
NSLog(#"%#", [error localizedDescription]);
}
And if I here do NSLog(#"%# - %#", wr.recoil, [item objectForKey:#"Recoil"]); I get the correct data. (Same data on both)
So. The correct data is obviously in the core. But my NSFetchRequest or something is failing. I am pretty noob at Objective-C, so it might be my bad code-grammar striking again. I realize I should use things again etc, not creating new objects all the time.. But cmon, this is my first app.. And if that is actually the problem, I might learn. But I'm stuck.
SOMETIMES I get data, sometimes I don't. It's weird. I re-launched the app, and got data from it, and now I don't.. I haven't found a pattern yet..
Anyone?
Or is there another way to request data from the entity?
I have some suggestions, too big for a comment.
1) after you create the WeaponsRanged, try reading them back:
for(NSDictionary *item in jsonWeaponRanged)
{
WeaponsRanged *wr = [NSEntityDescription insertNewObjectForEntityForName:#"WeaponsRanged"
inManagedObjectContext:context];
NSLog(#"IS WR Realized? %#", wr ? #"YES" : #"NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO WR");
///***///
wr.recoil = [item objectForKey:#"Recoil"];
///***///
NSError *error;
if(![context save:&error])
NSLog(#"%#", [error localizedDescription]);
}
// Now lets see if we can retrieve them:
{
NSEntityDescription *entiRange = [NSEntityDescription entityForName:#"WeaponsRanged" inManagedObjectContext:context];
NSFetchRequest *frRanged = [[NSFetchRequest alloc] init];
[frRanged setEntity:entiRange];
NSError *errorRanged = nil;
arrRange = [context executeFetchRequest:frRanged error:&errorRanged];
NSLog(#"Wrote %i items, read back %i items", [jsonWeaponRanged count], [arrRange count] );
}
2) In the viewController reading WeaponsRanged, add an assert before the fetch on mod:
NSLog(#"IS moc set? %#", moc ? #"YES" : #"NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO MOC");
EDIT:
3) Spread statements everywhere you access the MOC:
assert([NSThread isMainThread]);
[If you haven't used asserts before google and read up on the topic. These are a powerful tool for developers to find out about potential problems way before they manifest themselves in the gui or elsewhere. They are normally compiled out for release/distribution builds.]
This will force an exception if the thread is not the main thread, and then let you track down the reason by following the stack trace.
Nevermind! It was my own damn fault (yet again..).
The problem occured before the code I presented, and it turns out the data was never in the .sqlite-file when the problem was present.
This is what I had:
I collected data from the internet through json-request. I have told the app to check the "version" of the data through the internet, and if the data is outdated, then re-download it.
First, I download all data, then I add them to their own entity in Core Data. After downloading, I clear the current Core Data entity of the downloaded data. So on the top of each add-method it said i.e [self deleteAllObjectsOfEntity:#"WeaponsRanged"];, My whole problem was that in the addMelee-method, it ALSO said [self deleteAllObjectsOfEntity:#"WeaponsRanged"]; instead of #"WeaponsMelee", thus deleting all ranged weapons, and later adding melee to the melee entity. And that also proves that the other problem I mentioned of arrMelee logging out four times as much data as it should was caused by this.
The reason it sometimes worked was that the downloading is not happening in any ordered mode. So the addRanged was sometimes called before the addMelee. If ranged comes first, it clears the arrRanged, and fills it up with correct data, and THEN melee comes, and clears it out. When melee was called first, it cleared arrRanged and filled additional data to arrMelee, and THEN ranged comes and tries to clear an empty entity, and then fills it up with correct data.
The solution was obviously to change the entity deleted when adding it, as it was the wrong one.
Sorry.... :)

Deadlock in fetch routine

i am stuck with my first GCD and first core-data using application =)
two views access the same data (which is handled by a single DAO).
if i wait for the current view to finish loading its content no problem occors when changing view.
however: if i change the view (its tabbased) while one controller tries to fetch data from my model, the new controller tries the same and the threads 'collide' and my application freezes.
the freeze occurs in this line of code of my DAO:
NSArray *results = [managedObjectContext executeFetchRequest:fetch error:&error];
reloadAllMonth() accesses the fetch routine of my DAO
how i load the data in the first controller:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self reloadAllMonth];
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self.allMonthTable reloadData];
});
in the second viewcontroller the first thing i do is update my DAO, this of course uses (beneath others) the very same fetch routine i called before:
[self.dataHandler updateData];
i have tried two approaches so far:
first using a c-semaphore:
-(NSArray *)fetchAllMonthExpenses{
//#return: array of all expenses in month (day & month type)
NSNumber *monthNumber = [self getMonthNumber:[NSDate date]];
NSEntityDescription *exp = [NSEntityDescription entityForName:#"Expense" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch = [[NSFetchRequest alloc]init];
[fetch setEntity:exp];
[fetch setPredicate:[NSPredicate predicateWithFormat:#"month == %#",monthNumber]];
NSError *error = nil;
sem_wait(&isLoading);
NSArray *results = [self.managedObjectContext executeFetchRequest:fetch error:&error];
sem_post(&isLoading);
return results;
}
second using the synchronized directive
-(NSArray *)fetchAllMonthExpenses{
//#return: array of all expenses in month (day & month type)
NSNumber *monthNumber = [self getMonthNumber:[NSDate date]];
NSEntityDescription *exp = [NSEntityDescription entityForName:#"Expense" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch = [[NSFetchRequest alloc]init];
[fetch setEntity:exp];
[fetch setPredicate:[NSPredicate predicateWithFormat:#"month == %#",monthNumber]];
NSError *error = nil;
#synchronized(self.class){
NSArray *results = [self.managedObjectContext executeFetchRequest:fetch error:&error];
return results;
}
}
sadly both of the approaches did not work, the application freezes whatever i do.
so my question is: what am i doing wrong (as i mentioned first time using threads), what am i missing, where should i look?
this has been keeping me busy for 2 days now and i cant seem to wrap my head around it :/
An NSManagedObjectContext and all of the NSManagedObjects inside it are not thread safe.
Whatever thread you use to create the context, that needs to be the only thread where you do anything relating to that context. Even just reading values from one of the managed object must be done on that thread and not on any other thread.
If you need two threads which both deal with the same database, you've got two options:
use dispatch_sync() to jump into the other thread momentarily to perform all read/write operations on the managed objects and/or the context
Or:
create a second NSManagedObjectContext in the other thread for the same database, and keep any changes made to the two contexts in sync.
The first option is much easier, but may remove much of the benefits of threading. The second option is harder, but it can be done, and there is a fairly good API for keeping two contexts on different threads in sync.
Lookup the Core Data Programming Guide for more details.

Programmatically Update an attribute in Core Data

I've looked through all the class documentation for Core Data and I can't find away to programmatically update values in a core data entity. For example, I have a structure similar to this:
id | title
============
1 | Foo
2 | Bar
3 | FooFoo
Say that I want to update Bar to BarBar, I can't find any way to do this in any of the documentation.
In Core Data, an object is an object is an object - the database isn't a thing you throw commands at.
To update something that is persisted, you recreate it as an object, update it, and save it.
NSError *error = nil;
//This is your NSManagedObject subclass
Books * aBook = nil;
//Set up to get the thing you want to update
NSFetchRequest * request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"MyLibrary" inManagedObjectContext:context]];
[request setPredicate:[NSPredicate predicateWithFormat:#"Title=%#",#"Bar"]];
//Ask for it
aBook = [[context executeFetchRequest:request error:&error] lastObject];
[request release];
if (error) {
//Handle any errors
}
if (!aBook) {
//Nothing there to update
}
//Update the object
aBook.Title = #"BarBar";
//Save it
error = nil;
if (![context save:&error]) {
//Handle any error with the saving of the context
}
The Apple documentation on using managed objects in Core Data likely has your answer. In short, though, you should be able to do something like this:
NSError *saveError;
[bookTwo setTitle:#"BarBar"];
if (![managedObjectContext save:&saveError]) {
NSLog(#"Saving changes to book book two failed: %#", saveError);
} else {
// The changes to bookTwo have been persisted.
}
(Note: bookTwo must be a managed object that is associated with managedObjectContext for this example to work.)
Sounds like you're thinking in terms of an underlying relational database. Core Data's API is built around model objects, not relational databases.
An entity is a Cocoa object—an instance of NSManagedObject or some subclass of that. The entity's attributes are properties of the object. You use key-value coding or, if you implement a subclass, dot syntax or accessor methods to set those properties.
Evan DiBiase's answer shows one correct way to set the property—specifically, an accessor message. Here's dot syntax:
bookTwo.title = #"BarBar";
And KVC (which you can use with plain old NSManagedObject):
[bookTwo setValue:#"BarBar" forKey:#"title"];
If I'm understanding your question correctly, I think that all you need to keep in mind is managed objects are really no different than any other Cocoa class. Attributes have accessors and mutators you can use in code, through key value coding or through bindings, only in this case they're generated by Core Data. The only trick is you need to manually declare the generated accessors in your class file (if you have one) for your entity if you want to avoid having to use setValue:ForKey:. The documentation describes this in more detail, but the short answer is that you can select your attributes in the data model designer, and choose Copy Obj-C 2.0 Method Declarations from the Design menu.
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSArray *temp = [[NSArray alloc]initWithObjects:#"NewWord", nil];
[object setValue:[temp objectAtIndex:0] forKey:#"title"];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
I think this piece of code will give you the idea ;)