Core Data, Search Controller and Table View - iphone

I have integrated the search function to my app to query core data/.sqlite and it works ok. But I have a problem and I am not sure which class configuration should I look at, could someone points me to the light, thanks
Basically my model is this
TableView 1
Display Product Category
selectRow --> TableView2
TableView 2
Display Products of selected Category
selectRow --> TableView3
As I integrated the UISearchBar in TableView 1, I wish to have the function when people search the product they want and it will show up the product's name right away in the table view. I tried, but the result is showing the Category which contains the "searched product".
So, how could I get this to show up correctly and which section of configuration should I look at?
UISearchDisplayController *searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
[self performSelector:#selector(setSearchDisplayController:) withObject:searchDisplayController];
[searchDisplayController setDelegate:self];
[searchDisplayController setSearchResultsDataSource:self];
[searchDisplayController setSearchResultsDelegate:self];
[searchDisplayController release];
[self.tableView setContentOffset:CGPointMake(0,self.searchDisplayController.searchBar.frame.size.height)];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
self.filteredListContent = [NSMutableArray arrayWithCapacity:[[[self fetchedResultsController] fetchedObjects] count]];
}
Is it this part of the code?
Thanks
Update with more info:
Configure Cell
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *entity = nil;
if (self.searchIsActive){ // Configure the cell to show the searched item's name
entity = [[self filteredListContent] objectAtIndex:[indexPath row]];
cell.textLabel.textColor = [UIColor blackColor];
} else {// Configure the cell to show the category's name
cell.textLabel.textColor = [UIColor blackColor];
entity = [fetchedResultsController objectAtIndexPath:indexPath];
}
cell.textLabel.text = [entity valueForKey:#"nameEN"];
}
The Search Predicate
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"ANY products.nameEN CONTAINS[cd] %#", searchText];
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
}
The Core Data Structure
Category{
nameEN
products <- one to many relation ->> Product.productcat
}
Product{
nameEN
spec
productcat <<-- many to one relation-> Category.products
}
Thank you.

If your data model has a Product entity and a Category entity and your fetches are returning Category objects instead of Product objects, then you have the wrong entity set for your fetch.
[ignore following as it applies to a different type of search -- techzen]
You usually create a separate fetch for the search because every time the users enters new characters in the search, the predicate for the fetch must change.
Update:
Okay, I misunderstood the type of search you were implementing. You are filtering the return of an existing fetch instead of fetching based on the entered search.
Looking at the predicate and data model in your update I think it clear that the predicate will only work against an array of Category objects. This:
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"ANY products.nameEN CONTAINS[cd] %#", searchText];
... can only filter Category objects because only the Category object has an attribute of products. This predicate says, "Match all Catergory objects in which any related Product object has a nameEn attribute value that contains the search text."
Remember as well that the array you are filtering here:
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
...is an array of Category objects and not Product objects.
I think you need to rethink your UI design. Your TableView1 defaults to displaying a list of Category objects but you want your search of that table to display a list of Product objects. That will confuse the user. The user will expect a search on a table of Category objects to return a subset of Category objects.
However, with the existing design, you can produce an array of Product objects with the current code by creating a new array of Product objects by apply the #distinctUnionOfObjects collection operator:
self.filteredListContent = [[[self fetchedResultsController] fetchedObjects] filteredArrayUsingPredicate:predicate];
NSArray *distinctProducts=[self.filteredListContent valueForKey:#"products.#distinctUnionOfObjects.enName"];
... distinctProducts will now be an array of Product objects matching the search criteria. Use that array in configureCell:atIndexPath (you may need to resort it.)

So, I tried these at the configure cell part
NSDictionary *distinctProducts=[self.filteredListContent valueForKey:#"products"];
NSLog(#"what are products:%#",distinctProducts);
NSArray *listofProductsName = [distinctProducts valueForKey:#"nameEN"];
NSLog(#"whatup: %#",listofProductsName);
NSArray *entity = [listofProductsName objectAtIndex:indexPath.row];
cell.textLabel.textColor = [UIColor blackColor];
cell.textLabel.text = entity;
But then I could convert the name to show...it said _NFSet isEqualToString: ....etc terminated NSException.. though the NSLog of listofProductsName show up the right products name list.

Related

coredata sqlite malformed DB

I have problems with my coredata sqlite DB, which hosts a book DB. After a crash a user experiences has the problem, that the data isn't shown properly any more in his/her tableview.
This is due to the fact, that the performFetch method returns an error:
[NSFetchedResultsController deleteCacheWithName:nil];
if (![[self fetchedResultsController] performFetch:&error]) {
//NSArray *array = [[self fetchedResultsController] fetchedObjects];
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
#ifdef DEBUG
abort();
#endif
}
which results in this error message:
Unresolved error Error Domain=NSCocoaErrorDomain Code=134060 "The operation couldn’t be completed. (Cocoa error 134060.)" UserInfo=0x1dff80 {reason=The fetched object at index 312 has an out of order section name 'Z. Objects must be sorted by section name'}, {
reason = "The fetched object at index 312 has an out of order section name 'Z. Objects must be sorted by section name'";
When I have a look into the sqlite file with 'SQLite Database Browser 2.0 b1' the attributes of each entity seem to be ok.
When I delete some of the entities being mentioned everything works fine again.
I would like to know how I can find out what exactly is wrong with the mentioned entities and fix that, so the user can use his/her data again. Of course I want to fix the bug which causes the malformed DB as well but that is out of focus in this post.
Does anybody have any hints where I could have a look at or what might be malformed within my DB or what "an out of order section name" is?
This is the code for my fetchedResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity;
entity = [NSEntityDescription entityForName:#"Book" inManagedObjectContext:[[GlobalData sharedInstance] managedObjectContext]];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:10];
//set searchPredicate
NSPredicate *predicate = nil;
if (self.bibList != nil) {
predicate = [NSPredicate predicateWithFormat:#"ANY BibLists.name LIKE %#", self.bibList.name];
}
if (predicate) {
[fetchRequest setPredicate:predicate];
}
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor;
NSString *sortDescriptorString = nil;
sortDescriptorString = #"title";
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortDescriptorString ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSString *sectionNameKeyPath = nil;
sectionNameKeyPath = #"uppercaseFirstLetterOfTitle";
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController;
aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:[[GlobalData sharedInstance] managedObjectContext]
sectionNameKeyPath:sectionNameKeyPath
cacheName:#"Bibliography"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Thanks
b00tsy
I think what is going on here is that your SQL store is corrupted and/or you have an unusual sectionNameKeyPath.
The Core Data schema prefixes all the SQL column names with Z so an error of a name of 'Z suggest a corrupted SQL table. To test for that, execute the fetch request directly instead of using the fetched results controller and see if you can fetch all the objects.
If you can't, then the SQL store is corrupted. Most likely, some table component is simply name 'Z instead of something like ZAttributeName. You will have to edit the SQL directly but that is tricky because of the custom private schema. See this post to get an idea what to look for. There are not any tools for doing this because corruption of the store is so rare.
If the fetch works then the problem is with the sectionNameKeyPath being handed to the fetched results controller. Right now, it looks like you have an entity attribute with the first letter of the title attribute. (This is redundant because by default, the FRC will automatically return alphabetic sections based on any string attribute.) Try changing the sectionNameKeyPath to just the title attribute name (most likely "title"). In any case, just dispense with the uppercaseFirstLetterOfTitle attribute altogether.
The following statement in the documentation hints at the error.
If this key path is not the same as that specified by the first sort descriptor in fetchRequest, they must generate the same relative orderings.
It seems that the item at index 312 is not in the correct section when sorting on title alone. This could be a result of the title property beginning with a non-letter or lowercase letter, which using lexical sorting would put it after the Z section but the value of the item's uppercaseFirstLetterOfTitle is not 'Z'. As a suggestion, try adding a sort descriptor on the uppercaseFirstLetterOfTitle as the first sort descriptor, then add the sort descriptor on title.

iPhone SDK: Core Data and uniquing?

Either I'm not understanding what the term "uniquing" means in Core Data, or I'm not fetching my data properly. I have a pretty simple data model. Three entities: Community, Asset, and Category. Each Community has a relationship to multiple categories. Each category has a relationship to multiple assets. Each asset that is created must have one and only one category.
In the code I've posted, I'd like to output all the categories that a specific community has into the console. I thought that because of Core Data's uniquing capability, only one category of the same name could exist at a time (name is the only attribute for a category). However, when I print to the console, I'm getting duplicate category names.
// Fetch Community instances in the database, and add them to an NSMutableArray
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *community = [NSEntityDescription entityForName:#"Community" inManagedObjectContext:managedObjectContext];
[request setEntity:community];
// Only return the community instances that have the cityName of the cell tapped in the CommunitiesNonEditableTableViewController
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(cityName like %#)", cellCityName];
[request setPredicate:predicate];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
// Set communitiesArray with mutableFetchResults
[self setCommunitiesArray:mutableFetchResults];
[mutableFetchResults release];
[request release];
// Creates a community instance using the community stored in the array at index 0. This is the only community in the array.
Community *communityInstance;
communityInstance = [communitiesArray objectAtIndex:0];
// Retrieves existing categories of assets in the community, and adds them to an NSSet
NSSet *communityCategoriesSet = communityInstance.categories;
// Converts NSSet to an NSArray with each category as an index
NSArray *communityCategoriesArray = [communityCategoriesSet allObjects];
// For loop that iterates through the array full of categories, retrieves the names of each category, and adds it to an NSMutableArray
categoryNames = [[NSMutableArray alloc] init];
int i;
for (i = 0; i < [communityCategoriesArray count]; i++) {
Category *categoryInstance;
categoryInstance = [communityCategoriesArray objectAtIndex:i];
[categoryNames addObject:categoryInstance.name];
}
// Prints array full of category names to console
NSLog(#"%#", categoryNames);
When I execute this, I get duplicate names in the console. Why?
Uniquing means that each object in the object graph is itself unique. It does not mean that the attributes of any two objects are not identical. Uniquing is about relationship not attributes. No two objects can occupy the exact same position in the object graph.
As to why you get multiple categories in the output: The simplest explanation is that communityInstance.categories is a to-many relationship. (Since it has a plural name and you assign it to set.) In a to-many relationship, the context does not force a single object on the other end of the relationship.

Loading all the values of an attribute of core data to an array

I have an attribute "term" which is a NSString in my core data "Event".
When the table view is loaded I want all the values of "name" to be loaded to an array.
I used the code
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Event *event = nil;
event = [fetchedResultsController objectAtIndexPath:indexPath];
if(searching){
cell.textLabel.text = [copyListOfItems objectAtIndex:indexPath.row];
}else{
if(event.term){
[listOfItems addObject:event.term];
}
cell.textLabel.text = event.term;
cell.detailTextLabel.text = event.definition;
}
}
The problem is that all the terms are not loaded to the NSMutableArray listOfItems. It is loaded when the table cells is scrolled to bottom.
How to load all the contents of "term" from core data to the array listOfItems.
Any help will be greatly appreciated.
You are most likely looking at Core Data wrong. Core Data is not a database. It is an object graph that happens to persist to a database. Having said that, your question is better asked as "how can I load all instances of the entity 'Event' and access its term property".
To do that, you want to want to build and execute a NSFetchRequest against the 'Event' entity with no predicate.
NSManagedObjectContext *moc = ...;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Event" inManagedObjectContext:moc]];
NSError *error = nil;
NSArray *events = [moc executeFetchRequest:request error:&error];
NSAssert2(events != nil && error == nil, #"Error fetching events: %#\n%#", [error localizedDescription], [error userInfo]);
From here you can then use 'KVC' to retrieve an array of the name property:
NSArray *namesArray = [events valueForKey:#"name"];
Which will result in an array with just that string populated.
However the better question is, why do you want the array if you are displaying everything in a table? The NSFetchedResultsController already has all of the 'Event' entities retrieved for you; it is only the display that is bound to the table. What is your goal?
Update
Thank You very much for the detailed answer Mr Marcus... My goal is to search a particular "term" from the core data using search bar. Is there any better method for this to do?
Yes there is, and there are many solutions to the problem. You can access all of the results from the existing NSFetchedResultsController using its -fetchedObjects property. From there you can get a filtered array by applying a NSPredicate against that array using -filteredArrayUsingPredicate: and you can then use that array in your search results.
If you do a search on SO for Core Data and the UISearchDisplayController, I suspect you will turn up many results and suggestions.

How do I get Attributes from Core Data into an Array for - iPhone SDK

I'm trying to retrieve data from Core Data and put it into a Mutable Array
I have an Entity called 'Stock' and in Properties, attributes called : code, price & description...
How do I get the data stored in these attributes into a simple Mutable Array?
I've added this code...
NSMutableArray *array = [[NSMutableArray alloc]init];
[array addObject:[stock valueForKey:#"code"]];
and I get this error...
'-[NSCFArray insertObject:atIndex:]: attempt to insert nil'
I have a 'Managed Object Class' called 'Stock' and declared called stock. Am I missing something?
If I do this in the -cellForRowAtIndexPath...
Stock *stock1 = [fetchedResultsController objectAtIndexPath:indexPath];
array = [[NSMutableArray alloc] init];
[array addObject:stock1.code];
NSLog(#"Filtered List is? %#", array);
In the console I can see these 2 items
'The Filtered array is 810005'
'The Filtered array is 810007
'
What must I do to get these items(810005 & 810007) into an array set up in the -viewDidLoad method? Like it does in the -cellForRowAtIndexPath?
Update
Hi Marcus,
Finally got it working (well, 80%)
I put this in the -cellForRowAtIndexPath
Stock *product = nil;
if (tableView == self.searchDisplayController.searchResultsTableView)
{
filteredListContent = [NSMutableArray arrayWithObjects:stock1.code, nil];
product = [self.filteredListContent objectAtIndex:indexPath.row];
[self configureFilteredCell:cell atIndexPath:indexPath];
[filteredListContent objectAtIndex:indexPath.row];
NSLog(#"Filtered List Array List is? %#", stock1.code);
}
else
{
listContent = [NSMutableArray arrayWithObjects:stock1.code, nil];
[self configureCell:cell atIndexPath:indexPath];
NSLog(#"List Array List is? %#", stock1.code);
}
Then I used this code in the scope
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
self.savedSearchTerm = searchText;
if (searchText !=nil)
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"code beginsWith[cd] %#", searchText];
[fetchedResultsController.fetchRequest setPredicate:predicate];
}
else
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"code contains[cd] %#", searchText];
[fetchedResultsController.fetchRequest setPredicate:predicate];
[self.tableView reloadData];
}
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error])
{
// Handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
[self.tableView reloadData];
Everything is filtering fine but when I hit cancel on the search, it's not reloading the original data...
I won't be defeated...!!
Thanx
Since you are having this issue in your -viewDidLoad, I am guessing (and without the code from -viewDidLoad, it is only a guess) that you are trying to fetch objects from the NSFetchedResultsController before the -executeFetch: has been called on the controller and therefore you are in the land of nils.
I would suggest setting a break point in your -viewDidLoad and watching the values and you walk through your code. This will tell you what is nil and where.
Of course a better question is, why are you trying to put NSManagedObject instances into a NSMutableArray? Since they are already in your NSFetchedResultsController is there really a need to build up another array? What is the end goal?
Update
Now I understand what you are trying to do.
Solution 1
Only populate the array when a search has been conducted. Take a look at the http://developer.apple.com/iphone/library/samplecode/TableSearch/index.html example code and you should see how to apply it to your situation.
If you want to enter the table view with a pre-defined search then you need to perform it after you have executed a -performFetch: in the NSFetchedResultsController.
Solution 2
Modify the NSPredicate on the NSFetchedResultsController to include your search terms and then execute -performFetch: on the NSFetchedResultsController, you may have to do a -reloadData on the table as well, I am not sure.
When the user clears the search field you reset the predicate and re-fetch everything. Since it is all cached there should be no performance penalty.
Solution 2 just occurred to me and I have not tested it personally but there is no reason it shouldn't work just fine. Should even give you live updates within the search.
Have you read the documentation? You fetch your Stock instances (all of them or filter them with a predicate), then do with them whatever you please.
You can then add their properties to an array individually:
[array addObject:[stockInstance valueForKey:#"price"];
... or use a combination of < NSKeyValueCoding > protocol methods such as -dictionaryWithValuesForKeys: NSDictionary methods such as -objectsForKeys:notFoundMarker: to get an array for given keys.
This may or may not actually be what you need to do, though. It depends on what you intend to use the resulting array for. If you want a quick sum of all matching Stock instances' "price" values, for example, you can use Set and Array Operators. It really depends on what you're trying to achieve.
When I got your error,
'-[NSCFArray insertObject:atIndex:]: attempt to insert nil'
I had given the fetchedRequest a sort descriptor that had a nil key. The error appeared when I used these lines:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:nil ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
The error disappeared when I set the key to #"name":
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];

Iphone Core data question

I have set up the application with 2 entity, called Category1 and category2. There is a to-many relationship between category1 and category2. When a cell is pushed in tableview (category1), a new tableview will display all category2 cells related to category1. Here is what an example:
I have two category1 items in the first tableview, called Food and Snacks. Item "Food" have a subcategory (category 2) that contains 5 different kinds of food. Item "Snacks" have a subcateogry (category2) that contains 10 different snacks.
So when i push the food item (category1) I just want the food item to be loaded (5 of them). Right now, I can see all 5 of the food item in the tableView, plus the 10 items from the "snacks" category2.
I use this code in category2:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Folder" inManagedObjectContext: tag.managedObjectContext];
[request setEntity:entity];
//NSMutableSet *filtered = [tag mutableSetValueForKey:#"folders"];
// Order the events by creation date, most recent first.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"creationDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptor release];
[sortDescriptors release];
// Execute the fetch -- create a mutable copy of the result.
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[tag.managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil)
{
// Handle the error.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
// Set self's events array to the mutable array, then clean up.
[self setTagsArray:mutableFetchResults];
[mutableFetchResults release];
[request release];
need help !
thanks in advance!
First a bit of advice: In your root table view you should be using a NSFetchedRequestController to manage the top level objects. Take a look at the Recipes application in Apple's sample code for a demonstration.
As for the primary issue you are seeing, what you should do is push the category1 object to the child view controller. The child view controller can then query the that category1 object for all of its children to display. No fetch is required on the child view controller because you already have the parent object. Again, the recipe example app from Apple will demonstrate this quite clearly for you.
Do it in views, so there are two views for the subcategories, one for food, one for snacks. Then on the first table view have it set so when you tap on food, it'll go to the food view, same with the snacks. If you can't get it working try with UIButtons. What I do is if all else fails, go to UIButtons :) If you need help getting it to switch views I made an example as part of my 25 apps in august project, you can find my example here: http://appeveryday.wordpress.com/2009/08/02/app-3/