Well, I have been beating my head over this one for some time now, definitely over a week and I just don't know where I am going wrong. Your help would be greatly appreciated!!
Here is the app idea. I have two entities modeled in core data. The Tycoon entity has a one to many relationship with Speech, so each Tycoon is capable of having more than one speech.
In the Tycoon entity, the relationship is titled 'TycoonToSpeech' and in the Speech entity, the relationship to Tycoon is titled 'SpeechToTycoon' (this is not a one to many relationship).
Problem 1: I thought I had created my relationships in the appdelegate.m file correctly, but I am not sure. Here is the code in my appdelegate to generate content. Should I do anything more with the NSMutableSets???
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *tycoonEntity = [NSEntityDescription entityForName:#"Tycoon" inManagedObjectContext:context];
NSManagedObject *steve = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];
[steve setValue:#"Steve Jobs" forKey:#"Name"];
int orgId = [steve hash];
[steve setValue:[NSNumber numberWithInt:orgId] forKey:#"Id"];
NSManagedObject *warren = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];
[warren setValue:#"Warren Buffet" forKey:#"Name"];
int orgId2 = [warren hash];
[warren setValue:[NSNumber numberWithInt:orgId2] forKey:#"Id"];
NSEntityDescription *speechEntity = [NSEntityDescription entityForName:#"Speech" inManagedObjectContext:context];
NSManagedObject *stanford = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[stanford setValue:[NSNumber numberWithInt:[stanford hash]] forKey:#"speechId"];
[stanford setValue:#"Stanford" forKey:#"speechName"];
NSManagedObject *wwdc = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[wwdc setValue:[NSNumber numberWithInt:[wwdc hash]] forKey:#"speechId"];
[wwdc setValue:#"WWDC" forKey:#"speechName"];
NSManagedObject *shareHolder = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[shareHolder setValue:[NSNumber numberWithInt:[shareHolder hash]] forKey:#"speechId"];
[shareHolder setValue:#"Shareholder's Meeting" forKey:#"speechName"];
NSManagedObject *columbia = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[columbia setValue:[NSNumber numberWithInt:[columbia hash]] forKey:#"speechId"];
[columbia setValue:#"Columbia" forKey:#"speechName"];
NSMutableSet *jobsSpeeches = [steve mutableSetValueForKey:#"TycoonToSpeech"];
[jobsSpeeches addObject:stanford];
[jobsSpeeches addObject:wwdc];
NSMutableSet *warrenSpeeches = [warren mutableSetValueForKey:#"TycoonToSpeech"];
[warrenSpeeches addObject:shareHolder];
[warrenSpeeches addObject:columbia];
Problem 2: The main and really annoying problem, I don't know how to make didSelectRowAtIndexPath in my root view controller work so that when I click on 'Steve Jobs' in the table view, it brings up only his speeches and when I click on Warren Buffet it only brings up his speeches. I have banged my head against the wall trying to get this right. At the moment, when I click on either Steve Jobs or Warren Buffet it takes me to a new table view showing 'Steve Jobs' and 'Warren Buffet', so basically the exact same table view as the original!!! Here is my code for didSelectRowAtIndexPath.
// Create a new table view of this very same class.
RootViewController *rootViewController = [[RootViewController alloc]
initWithStyle:UITableViewStylePlain];
// Pass the managed object context
rootViewController.context = self.context;
NSPredicate *predicate = nil;
// Get the object the user selected from the array
NSManagedObject *selectedObject = [entityArray objectAtIndex:indexPath.row];
rootViewController.entityName = #"Speeches";
// Create a query predicate to find all child objects of the selected object.
predicate = [NSPredicate predicateWithFormat:#"SpeechToTycoon == %#", selectedObject];
[rootViewController setEntitySearchPredicate:predicate];
//Push the new table view on the stack
[self.navigationController pushViewController:rootViewController animated:YES];
[rootViewController release];
Newly Edited here**
OK, here is the main code in my SpeechViewController.m file. The numberOfRowsInSection method seems to be working, but the real problem is the cellForRowAtIndexPath that is the problem. I know I am probably doing something very stupid, but I don't know what it is.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSArray *speeches = (NSArray *)tycoon.TycoonToSpeech;
// Configure the cell...
cell.textLabel.text = [[speeches objectAtIndex:indexPath.row] retain];
return cell;
}
New speech view controller to language points view controller
I added this in the app delegate to add objects to my language points entity.
NSEntityDescription *langPointEntity = [NSEntityDescription entityForName:#"LanguagePoints" inManagedObjectContext:context];
NSManagedObject *pointOne = [NSEntityDescription insertNewObjectForEntityForName:[langPointEntity name] inManagedObjectContext:context];
[pointOne setValue:[NSNumber numberWithInt:[pointOne hash]] forKey:#"LangId"];
[pointOne setValue:#"Drop out" forKey:#"LangPoint"];
[pointOne setValue:stanford forKey:#"LanguagePointsToSpeech"];
Then in my speechViewController.m file I created an NSArray called speechInfo, then here is my didSelectRowAtIndexPath method.
LangPointsViewController *langPointsView = [[LangPointsViewController alloc] initWithNibName:#"LangPointsViewController" bundle:[NSBundle mainBundle]];
Speech *speech = [speechInfo objectAtIndex:indexPath.row];
langPointsView.speech = speech;
[self.navigationController pushViewController:langPointsView animated:YES];
[langPointsView release];
I think this is where things are going wrong, I don't think I am getting the object here correctly because I do an NSLog on speech.Name in the LangPointsViewController and it comes up null, where as if I do an NSLog for tycoon.Name in the SpeechViewController it prints the person's name I clicked on. Is it the NSArray that's incorrect.
Problem #1
First your relationship names are bad. They should start with a lower case and they should simply describe what is on the other side of the relationship. You already know where you are. So name them tycoon and speeches respectively. Second, because relationships are bi-directional you can construct your data easier, see the following:
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *steve = [NSEntityDescription insertNewObjectForEntityForName:#"Tycoon" inManagedObjectContext:context];
[steve setValue:#"Steve Jobs" forKey:#"Name"];
int orgId = [steve hash];
[steve setValue:[NSNumber numberWithInt:orgId] forKey:#"Id"];
NSManagedObject *warren = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];
[warren setValue:#"Warren Buffet" forKey:#"Name"];
int orgId2 = [warren hash];
[warren setValue:[NSNumber numberWithInt:orgId2] forKey:#"Id"];
NSManagedObject *stanford = [NSEntityDescription insertNewObjectForEntityForName:#"Speech" inManagedObjectContext:context];
[stanford setValue:[NSNumber numberWithInt:[stanford hash]] forKey:#"speechId"];
[stanford setValue:#"Stanford" forKey:#"speechName"];
[stanford setValue:warren forKey:#"tycoon"];
NSManagedObject *wwdc = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[wwdc setValue:[NSNumber numberWithInt:[wwdc hash]] forKey:#"speechId"];
[wwdc setValue:#"WWDC" forKey:#"speechName"];
[wwdc setValue:warren forKey:#"tycoon"];
NSManagedObject *shareHolder = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[shareHolder setValue:[NSNumber numberWithInt:[shareHolder hash]] forKey:#"speechId"];
[shareHolder setValue:#"Shareholder's Meeting" forKey:#"speechName"];
[shareHolder setValue:warren forKey:#"tycoon"];
NSManagedObject *columbia = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[columbia setValue:[NSNumber numberWithInt:[columbia hash]] forKey:#"speechId"];
[columbia setValue:#"Columbia" forKey:#"speechName"];
[columbia setValue:warren forKey:#"tycoon"];
Note that I am setting the relationship from the Speech side. Core Data will handle the other side.
Problem #2
Without looking at your entire root view controller it is hard to say what you are doing "wrong". However, I would not re-use a view controller to display two different types of objects. That is bad form and leads to code cruft.
Instead I would create a second "master" view controller that is designed to handle speeches. Then you can just pass in the tycoon and your SpeechViewController can run just off an array derived from the relationship or it can build up its fetched results controller internally. Even of greater value is the ability to customize its cells, title, etc. to fit the data it is displaying.
Problem #3
First a comment about the new code you added:
SpeechViewController *speechView = [[SpeechViewController alloc] initWithNibName:#"SpeechViewController" bundle:[NSBundle mainBundle]];
Tycoon *tycoons = (Tycoon *)[fetchedResultsController objectAtIndexPath:indexPath];
speechView.tycoons = tycoons;
[self.navigationController pushViewController:speechView animated:YES];
There is ZERO reason to cast out of a -[NSFetchedResultsController objectAtIndexPath:]. Any method that returns id can be assigned to any pointer.
Casting is a lie to the compiler and should be avoided at all cost.
Now, leaving that aside, your code is fine. I would name the property 'tycoon' instead of 'tycoons' because you are passing in a single Tycoon entity.
On the SpeechViewController side you can now access the speeches associated with that Tycoon entity and display them however you choose.
What is the issue you are seeing?
Problem #4
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSSet *speechesSet = [[self tycoon] valueForKey:#"TycoonToSpeech"];
// Sort the speeches
NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortedSpeeches = [speechesSet sortedArrayUsingDescriptors:[NSArray arrayWithObject:nameSort]];
// Configure the cell...
id speech = [sortedSpeeches objectAtIndex:[indexPath row]];
[[cell textLabel] setText:[speech valueForKey:#"name"]];
return cell;
}
Ok starting at the top. First, a relationship will return a NSSet not a NSArray. The results from a relationship are unordered. Therefore we grab the results as a NSSet and then sort them. I guessed that there is a name attribute on your Speech entity.
Once we have them sorted we can grab the item at the row. This will return a Speech entity. I assigned it to id because we are going to be using KVC anyway.
Finally, I use KVC to get the name attribute from the Speech entity and assign it to the textLabel of the cell.
As for your -tableView: numberOfRowsInSection:, that one is far simpler:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[[self tycoon] valueForKey:#"TycoonToSpeech"] count];
}
Because you only have one section you only need to return the count of speeches.
I mentioned before that your attribute names need some work. Since Tycoon is just an object and TycoonToSpeech is just a property on that object, the property should really be called speeches because it is no more special than any other property on an object. The attribute name flows better in your code and makes your code easier to consume. In addition, Objective-C naming conventions call for attributes aka properties to start with a lower case.
Related
I have a UICollectionView that needs to be updated when the model has changed.
It is subscribed to the notification center in order to do so:
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(modeloActualizado:) name:NSManagedObjectContextObjectsDidChangeNotification object:self.contexto];
}
The UICollectionViewController connects to the model through a NSFetchedResultsController that is meant to return a list of the entities "Plaza" whose "ocupada" value is 1 (it is a boolean).
-(NSFetchedResultsController*) frController{
if(_frController == nil){
NSFetchRequest *request = [[NSFetchRequest alloc]init];
NSEntityDescription *entidad = [NSEntityDescription entityForName:#"Plaza" inManagedObjectContext:self.contexto];
request.entity = entidad;
request.fetchBatchSize = 10;
// AƱadir un predicado para filtrar por plazas ocupadas
request.predicate = [NSPredicate predicateWithFormat:#"ocupada == 1"];//%#",[NSNumber numberWithBool:YES]];
// Ordenar por numero de plaza
NSSortDescriptor *ordenPorNumero = [[NSSortDescriptor alloc] initWithKey:#"numero" ascending:YES];
NSArray *descriptores = [[NSArray alloc] initWithObjects:ordenPorNumero, nil];
[request setSortDescriptors:descriptores];
// Crear el FetchedResultsController
//[NSFetchedResultsController deleteCacheWithName:#"Coleccion"];
_frController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.contexto sectionNameKeyPath:nil cacheName:#"Coleccion"];
_frController.delegate = self;
NSError *error = nil;
if(![self.frController performFetch:&error]){
NSLog(#"Ha ocurrido un error: %# %#",error,[error userInfo]);
abort();
}
}
return _frController;
}
The method called when the model changed does reload the CollectionView data:
-(void)modeloActualizado:(NSNotification *) notificacion{
[self.collectionView reloadData];
}
The problem is, if I change any of the values of an entity, those changes are shown in the CollectionView, but if I change the entity boolean value "ocupada" to NO, it will still appear into the CollectionView until the App is closed and opened again.
Am I doing something wrong? I don't know why the NSFetchedResultsController is returning the same amount of objects even though one of them no longer matches the predicate condition.
Any ideas?
Thanks in advance.
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.
I am using core data and trying to populate a UITableView with an NSMutableSet. I have two entities, Teams and Players. On my addTeamsController I am saving a player to the team as follows
-(void)saveButtonWasPressed {
self.team =[NSEntityDescription insertNewObjectForEntityForName:#"Team" inManagedObjectContext:self.managedObjectContext];
Player *newPlayer = (Player *)[NSEntityDescription insertNewObjectForEntityForName:#"Player"
inManagedObjectContext:self.managedObjectContext];
team.schoolName = _schoolName.text;
team.teamName = _teamName.text;
team.teamID = _teamName.text;
team.season = _season.text;
team.headCoach = _headCoach.text;
team.astCoach = _assistantCoach.text;
[self.team addPlayers:_tempSet];
[self.managedObjectContext save:nil];
[self.navigationController popViewControllerAnimated:YES];
}
On another viewController I am trying to populate a tableview with that teams players. To do that I am doing as follows
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"firstName" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
_array = [[_team.players allObjects] sortedArrayUsingDescriptors:sortDescriptors];
and then on my cell for row and index path I am doing the following
cell.textLabel.text = [_array objectAtIndex:indexPath.row];
And I get the error
[Player isEqualToString:]: unrecognized selector sent to instance
I am wondering what the best approach to filling the tableview sorted by the players first names is.
The best way to populate a table from a core data store is to use an NSFetchedResultController. But that is not going to fix the problem you're having, the problem is that your trying to set cell.textLabel.text to an NSManagedObject, which doesn't work. You can set
cell.textLabel.text = Player.someStringAttribute
This question is about Core Data.
I created a Entity called TV with three attributes called name, price and size. I also created a subclass of NSMutableObject with TV.h and TV.m files.
I imported the TV.h to my DetailViewController.h which handles my sliders und UIElements I want to take the values of.
So I did a fetch request, and everything works fine, BUT:
Everytime I update the UISlider (valueDidChange:), Xcode creates a COPY of my entity and adds it to my TV-Object.
All I want Xcode is just to edit and save to the current entity, not to edit and save in a new entity.
Help is very appreciated!
Thank you in advance.
My Code:
DetailViewController.m
- (IBAction)collectSliderValue:(UISlider *)sender {
if (__managedObjectContext == nil) {
NSLog(#"Problem ...");
__managedObjectContext = [(MasterViewController *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(#"... solved!");
}
if (sender == sizeSlider) {
NSError *error = nil;
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"TV" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
TV * currentTV = [[TV alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
currentTV.size = [[NSNumber alloc] initWithInt:(sender.value + 0.5f)];
currentTV.name = #"New TV!";
NSError *error11;
[__managedObjectContext save:&error11];
for (NSManagedObject *info in fetchedObjects)
{
NSLog(#"Name = %#", [info valueForKey:#"name"]);
NSLog(#"Size = %#", [info valueForKey:#"size"]);
NSLog(#"Price = %#", [info valueForKey:#"price"]);
}
[fetchRequest release];
}
//Editing begins ...
TV * currentTV = [[TV alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
Editing doesn't begin, you are creating a new object right there. Your view controller needs an instance variable to hold the current TV entity that you are modifying.
From the template project you have created, the variable detailItem contains the managed object that you are currently editing. You should specifically set this as a TV object, and refer to this instead of currentTV in your detailViewController code. You must remove all of the fetch request and managed object context code - this is not relevant in your detail view controller, it should be managed by the master view controller.
So, in DetailViewController.h:
#property (strong, nonatomic) id detailItem;
becomes
#property (strong, nonatomic) TV detailItem;
And in your collectSliderValue method, it should look something much more simple like this:
- (IBAction)collectSliderValue:(UISlider *)sender
{
if (sender == sizeSlider)
self.detailItem.size = [NSNumber numberWithFloat:sender.value];
}
The saving of the managed object context shouldn't occur until back in your detail view controller, this is taken care of in your application delegate.
In your master detail controller .m file you may also need to import the TV.h file so that it knows that TV is a NSManagedObject subclass. Also, cast to TV when you are setting the detail item:
self.detailViewController.detailItem = (TV*)selectedObject;
I have an NSArray of strings and I want to add a certain amount of rows to the outline view depending on how many strings are in the array, each with the title of the String that was added.
I think it would involve looping through the array like this.
for(NSString *title in array) {
JGManagedObject *theParent =
[NSEntityDescription insertNewObjectForEntityForName:#"projects"
inManagedObjectContext:managedObjectContext];
[theParent setValue:nil forKey:#"parent"];
[theParent setValue:#"Project" forKey:#"name"];
[theParent setValue:[NSNumber numberWithInt:0] forKey:#"position"];
}
Don't bother. The tree controller sits between the model (The Core data store that Spark is using) and the view (your source view). Instead of adding from the array to the tree controller, you should be adding from the array to the data store.
The tree controller will pick up the changes in the model and show the changes in the view.
Edit:
(Bear in mind it is hard to debug from a distance.)
With Garbage Collection, if you don't hold on to your objects, they are liable to be cleaned up from underneath you.
Try this and see what happens:
for(NSString *title in array) {
NSManagedObjectContext *moc = [self managedObjectContext];
JGManagedObject *theParent =
[NSEntityDescription insertNewObjectForEntityForName:#"projects"
inManagedObjectContext:moc];
[theParent setValue:nil forKey:#"parent"];
// This is where you add the title from the string array
[theParent setValue:title forKey:#"name"];
[theParent setValue:[NSNumber numberWithInt:0] forKey:#"position"];
}