When sorting a table of objects from Core Data, I'd like to set a custom string for the section heading that includes an attribute. For example, I'd like the section name to display "4 Stars", instead of just 4. I've fiddle with it, but It seems to get grumpy if I try to set the string for the sectionNameKeyPath to anything other than an Entity Attribute and only an entity attribute. Here's what works for attribute only, and one of a few attempts to customize the string which breaks is commented out.
NSSortDescriptor *ratingDescriptor = [[NSSortDescriptor alloc] initWithKey:#"starRating" ascending:NO];
sortDescriptors = [NSArray arrayWithObjects:ratingDescriptor, nameDescriptor, nil];
[ratingDescriptor release], ratingDescriptor = nil;
// NSString *starSectionHeading = [NSString stringWithFormat:#"%d Stars", #"starRating"];
// sectionKeyPath = starSectionHeading;
sectionKeyPath = #"starRating";
Set your sectionNameKeyPath to the "starRating" but then modify the output in the table view. The FRC will sort things and tidy things up in sections you just have to change what you would normally display as the header string.
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
// Display the stars as section headings.
int stars = [[[[fetchedResultsController sections] objectAtIndex:section] valueForKey:#"name"] intValue];
if(stars == 1)
{
return #"1 Star"
}
else
{
return [NSString stringWithFormat:#"%u Stars", stars];
}
}
I do this in some table views where the output format is handled in a generic fashion (I delegate the header titles to a another controller class given the first sort descriptor path and the value of the title). So you are not limited to hard coding the table view delegate methods like the above code.
You also get a chance to localize the string here as well, I have to deal with 15 localizations in my app and you have to think about things a bit differently when localizing.
The sectionNameKeyPath is supposed to be a key path i.e. the name of single attribute or the name of a relationship that terminates in a single attribute. You are trying to create a composite of two attributes and the FRC does not support that automatically.
To get something more fancy you will have to subclass NSFetchedResultsController. From the docs.
You create a subclass of this class if
you want to customize the creation of
sections and index titles. You
override
sectionIndexTitleForSectionName: if
you want the section index title to be
something other than the capitalized
first letter of the section name. You
override sectionIndexTitles if you
want the index titles to be something
other than the array created by
calling
sectionIndexTitleForSectionName: on
all the known sections.
Look at this answer with creating transient attribute:
NSFetchedResultsController with sections created by first letter of a string
just change some names and in your version of committeeNameInitial replace:
[[self committeeName] substringToIndex:1];
with
[NSString stringWithFormat:#"%# Stars", [self starRating]];
(NSString *)tableView:(UITableView *)aTableView titleForHeaderInSection:(NSInteger)section
{
if ([self.fetchedResultsController sections].count > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo =
[[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
return nil;
}
Related
Here i need to hide the phone number, email , birthDate, anniversary date and other labels in case there is no values for those fields. How can i do this?
Many ways, starting with the simplest:
self.emailLabel.hidden = YES;
But you probably want to reformat the other parts of the view to fit the empty space. Keeping it simple, you would then do something like this:
self.phoneLabel.frame = CGRectOffset(self.phoneLabel.frame, 0, -self.emailLabel.bounds.size.height);
... and so on for anything below. But you can see how this would become tedious. The next and probably best alternative is a UITableView that adjusts it's section count based on whether some of that data is present. That would go like this. Prepare a mutable array of arrays with the parts of your model and their values.
- (void)prepareModel {
self.model = [NSMutableArray array];
[self.model addObject:#[#"Name", #"Judy"]; // get "Judy" from your data
if (/* model has email */) {
[self.model addObject:#[#"Email", #"judy#gmail.com"]; // get email from your model
}
// and so on for conditional parts of your model
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.model.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
return self.model[section][0];
}
And the cellForRow would init the cell using self.model[section][1].
What you can do is simply hide the UILabel's if the value for the NSStrings that you are putting them in is NULL/nil .
NSString *labelString;
if([labelString length]>0)
{
}
else
Label.hidden = YES;
It is probably a better idea to use a UITableView to do this, by putting the labels in rows of the tables. If the labels are empty, you can delete the table rows and iOS will dynamically resize the table height for you.
I have a table view and adding items to that like this.`
NSDictionary *memberInfo = [self.currentChannel infoForMemberWithID:memberID];
memberinfo=memberInfo;
cell.textLabel.text = [NSString stringWithFormat:#"%#",[memberinfo objectForKey:#"name"]];
cell.detailTextLabel.text = [NSString stringWithFormat:#"%#",[memberinfo objectForKey:#"status"]];
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.detailTextLabel.backgroundColor = [UIColor clearColor];
return cell;
`and it was working fine.Now i want to add a searchbar on to that.i need to load the tableview according to the matched string.But i am loading the tableview like this.I know how to search in an array.I am using
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText.i want to load the table view according to the match of the searchstring ,Can anybody know this?
You can use NSPredicate inside your SearchBar delegate to filter your main array (members or whatever you named it) and catch the results in an another global declared array. Now reload your table with this filtered array.
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[_mArrFilteredList removeAllObjects]; //global array which will contain filtered results
NSString* searchStr = [NSString stringWithFormat:#"*%#*",_srbActivity.text];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"%K == %#",#"name", searchStr]; //will filter according to "name" key in your dictionary.
[_mArrFilteredList addObjectsFromArray:[_mArrList filteredArrayUsingPredicate:predicate]]; //_mArrList is your main array which has all the dictionaries.
[yourTableView reloadData];
}
Now in your tableview datasource methods, populate table using filtered array(_mArrFilteredList).
For any search implementations we require two lists, one original and other filtered. We will usually use filtered list to fill up the table.
In your case, you can have dictionary as original list. For filtered list you can have a NSArray of values or keys according to requirements.
Filter the array list in search function, reload the table. In cellForRow get the object from the array keys or get key from array values- later object for that key.
In my 'Sectioned' UITableView I have two sections, the first one for Attributes like name and the second one is editable where you can add objects.
Here's a picture of it:
alt text http://fwdr.org/bm9w
There's an attribute that the user can change (in this case Type) which would to change the number of rows in second section. To be more specific, if one property is selected the maximum number of rows is 2 and then there would be no Add New… row.
Here's what I've tried (in my UITableViewController) …
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self isToManyRelationshipSection:section]) {
NSArray *sectionKeys = [rowKeys objectAtIndex:section];
NSString *row0Key = [sectionKeys objectAtIndex:0];
if ([[NSString stringWithFormat:#"%#", [managedObject valueForKey:#"type"]]
isEqualToString:[NSString stringWithFormat:#"One on One"]] && [[managedObject valueForKey:row0Key] count] == 2){
return [[managedObject valueForKey:row0Key] count];
}
return [[managedObject valueForKey:row0Key] count] +1;
}
return [rowLabels countOfNestedArray:section];
}
But if the user tries to remove a row when there are already 2 rows the app crashes because there would be the same number of rows before and after the deletion.
How do I get around this and do this properly?
First off, the numberOfRows.. method wouldn't appear to me as the proper place to do this kind of logic. I'd rather create a seperate method to determine and return the current game mode (and keep the gamemode updated whenever the user selects another mode). I'd associate integers with gamemodes (1 = "one on one", 2 = "all against all"...), that way in the numberOfRows Method you just need to check "is the current section == 1?" && "which game mode are we in?" to determine how many rows are needed.
If you keep track of the gamemode properly, you don't need to check for equal strings anymore, too.
I ended up doing this differently to what I first wanted to do.
My reasoning was that my current implementation gave me an error in the debugger which in turn crashed the app. This error seemed un-avoidable so I decided to simply disable the cell if there are two rows, instead of hiding it.
To do this in my tableView:cellForRowAtIndexPath: method I added the following code (in my if statement) to make it look un-selectable to the user:
cell.textLabel.textColor = [UIColor lightGrayColor];
cell.editingAccessoryType = UITableViewCellAccessoryNone;
And in the tableView:didSelectRowAtIndexPath: method I left my if statement blank so it wouldn't push another view controller or add a object. Although I did include this code to deselect the row:
[self.tableView deselectRowAtIndexPath:indexPath animated:NO];
My Requirement:
I have this straight forward requirement of listing names of people in alphabetical order in a Indexed table view with index titles being the starting letter of alphabets (additionally a search icon at the top and # to display misc values which start with a number and other special characters).
What I have done so far:
1. I am using core data for storage and "last_name" is modelled as a String property in the Contacts entity
2.I am using a NSFetchedResultsController to display the sorted indexed table view.
Issues accomplishing my requirement:
1. First up, I couldn't get the section index titles to be the first letter of alphabets. Dave's suggestion in the following post, helped me achieve the same: NSFetchedResultsController with sections created by first letter of a string
The only issue I encountered with Dave' suggestion is that I couldn't get the misc named grouped under "#" index.
What I have tried:
1. I tried adding a custom compare method to NSString (category) to check how the comparison and section is made but that custom method doesn't get called when specified in the NSSortDescriptor selector.
Here is some code:
#interface NSString (SortString)
-(NSComparisonResult) customCompare: (NSString*) aStirng;
#end
#implementation NSString (SortString)
-(NSComparisonResult) customCompare:(NSString *)aString
{
NSLog(#"Custom compare called to compare : %# and %#",self,aString);
return [self caseInsensitiveCompare:aString];
}
#end
Code to fetch data:
NSArray *sortDescriptors = [NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:#"last_name"
ascending:YES selector:#selector(customCompare:)] autorelease]];
[fetchRequest setSortDescriptors:sortDescriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:#"lastNameInitial" cacheName:#"MyCache"];
Can you let me know what I am missing and how the requirement can be accomplished ?
This is a really inefficient first-pass at this problem, which I am going to rewrite eventually. But hopefully this will help you.
The idea of this is to "guarantee" getting a real table section index back when tapping a "standard" section index view. A standard section index view should have a magnifying lens icon for search, a hash mark (#) for non-alphabetical sections, and letters A through Z for alphabetical sections.
This standard view is presented regardless of how many real sections there are, or what they are made of.
Ultimately, this code maps section view indices to real-existing alphabetic section name paths in the fetched results controller, or to real-existing non-alphabetic (numerical) sections, or to the search field in the table header.
The user will only occasionally recreate the section index mapping array (_idxArray) on each touch of the section index, but recreating the array on each touch is obviously inefficient and could be tweaked to cache pre-calculated results.
There are a lot of places to start tightening this up: I could make the sectionIndexTitleLetters static string all uppercase from the start, for example. It's fast enough on a 3GS phone, though, so I haven't revisited this recently.
In the header:
static NSString *sectionIndexTitleLetters = #"abcdefghijklmnopqrstuvwxyz";
In the implementation of the table view data source:
- (NSArray *) sectionIndexTitlesForTableView:(UITableView *)tv {
if (tv != searchDisplayController.searchResultsTableView) {
NSMutableArray *_indexArray = [NSMutableArray arrayWithCapacity:([sectionIndexTitleLetters length]+2)];
[_indexArray addObject:#"{search}"];
[_indexArray addObject:#"#"];
for (unsigned int _charIdx = 0; _charIdx < [sectionIndexTitleLetters length]; _charIdx++) {
char _indexChar[2] = { toupper([sectionIndexTitleLetters characterAtIndex:_charIdx]), '\0'};
[_indexArray addObject:[NSString stringWithCString:_indexChar encoding:NSUTF8StringEncoding]];
}
return _indexArray;
}
return nil;
}
- (NSInteger) tableView:(UITableView *)tv sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
if (tv != searchDisplayController.searchResultsTableView) {
if (index == 0) {
//
// This is the search bar "section"
//
[currentTableView scrollRectToVisible:[[currentTableView tableHeaderView] bounds] animated:YES];
return -1;
}
else if (index == 1) {
//
// This is the "#" section, which covers non-alphabetic section headers (e.g. digits 0-9)
//
return 0;
}
else {
//
// This is a bit more involved because the section index array may contain indices that do not exist in the
// fetched results controller's sections->name info.
//
// What we are doing here is building a "fake-index" array that will return a real section index regardless of
// whether the section index title being touched exists or not.
//
// The fake array will be of length of the section index title array, and each index will contain an unsigned
// integer from 1 to {numOfRealSections}.
//
// The value this array returns will be "nearest" to the real section that is in the fetched results controller.
//
NSUInteger _alphabeticIndex = index-2;
unsigned int _idxArray[26];
for (unsigned int _initIdx = 0; _initIdx < [sectionIndexTitleLetters length]; _initIdx++) {
_idxArray[_initIdx] = [[fetchedResultsController sections] count] - 1;
}
unsigned int _previousChunkIdx = 0;
NSNumberFormatter *_numberFormatter = [[NSNumberFormatter alloc] init];
NSLocale *_enUSLocale = [[NSLocale alloc] initWithLocaleIdentifier: #"en_US"];
[_numberFormatter setLocale:_enUSLocale];
[_enUSLocale release];
for (unsigned int _sectionIdx = 0; _sectionIdx < [[fetchedResultsController sections] count]; _sectionIdx++) {
NSString *_sectionTitle = [[[fetchedResultsController sections] objectAtIndex:_sectionIdx] name];
if (![_numberFormatter numberFromString:_sectionTitle]) {
// what's the index of the _sectionTitle across sectionIndexTitleLetters?
for (unsigned int _titleCharIdx = 0; _titleCharIdx < [sectionIndexTitleLetters length]; _titleCharIdx++) {
NSString *_titleCharStr = [[sectionIndexTitleLetters substringWithRange:NSMakeRange(_titleCharIdx, 1)] uppercaseString];
if ([_titleCharStr isEqualToString:_sectionTitle]) {
// put a chunk of _sectionIdx into _idxArray
unsigned int _currentChunkIdx;
for (_currentChunkIdx = _previousChunkIdx; _currentChunkIdx < _titleCharIdx; _currentChunkIdx++) {
_idxArray[_currentChunkIdx] = _sectionIdx - 1;
}
_previousChunkIdx = _currentChunkIdx;
break;
}
}
}
}
[_numberFormatter release];
return (NSInteger)_idxArray[_alphabeticIndex];
}
}
return 0;
}
I might be naive, but I don't understand why these solutions are so baroque. I did this:
In my model, I added a method:
-(NSString *)lastInitial {
return [self.lastname substringToIndex:1];
}
And in my tablecontroller I set the fetchedresultscontroller to use that method:
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"lastInitial" cacheName:#"Master"];
Seems to work - is there a reason that this is a bad idea? Or am I benefitting from new features in ios5 or something?
I have trouble to add new item to my table view with core data. Here is the brief logic in my codes. In my ViewController class, I have a button to trigle the edit mode:
- (void) toggleEditing {
UITableView *tv = (UITableView *)self.view;
if (isEdit) // class level flag for editing
{
self.newEntity = [NSEntityDescription insertNewObjectForEntityName:#"entity1"
inManagedObjectContext:managedObjectContext];
NSArray *insertIndexPaths = [NSArray arrayWithObjects:
[NSInextPath indexPathForRow:0 inSection:0], nil]; // empty at beginning so hard code numbers here.
[tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView setEditing:YES animated:YES]; // enable editing mode
}
else { ...}
}
In this block of codes, I added a new item to my current managed object context first, and then I added a new row to my tv. I think that both the number of objects in my data source or context and the number of rows in my table view should be 1.
However, I got an exception in the event of tabView:numberOfRowsInSection:
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted).
The exception was raised right after the delegate event:
- (NSInteger) tableView:(UITableView *) tableView numberOfRawsInSection:(NSInteger) section {
// fetchedResultsController is class member var NSFetchedResultsController
id <NSFechedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections]
objectAtIndex: section];
NSInteger rows = [sectionInfo numberOfObjects];
return rows;
}
In debug mode, I found that the rows was still 0 and the event invoked after the the even of toggleEditing. It looks like that sectionInfo obtained from fetchedResultsController did not include the new entity object inserted. Not sure if I miss anything or steps? I am not sure how it works: to get the fetcedResultsController notified or reflect the change when a new entity is inserted into the current managed object context?
I think I got a solution. Actually, I don't need to create entity in the toggleEditing event. Then entity object should be created when an insert event is committed. Here is the update of my codes in the toggleEditing event:
- (void) toggleEditing {
UITableView *tv = (UITableView *)self.view;
if (isEdit) // class level flag for editing
{
insertRows = 1; // NSInteger value defined in the class or header
NSArray *insertIndexPaths = [NSArray arrayWithObjects:
[NSInextPath indexPathForRow:0 inSection:0], nil]; // empty at beginning so hard code numbers here.
[tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView setEditing:YES animated:YES]; // enable editing mode
}
else { insertRows = 0; ...}
}
In the event, a row is dynamically inserted into the current table view. Since a new row is added, in the following delegate, I have to make sure the rows returned in the section reflects the incitement:
- (NSInteger) tableView:(UITableView *) tableView numberOfRawsInSection:(NSInteger) section {
// fetchedResultsController is class member var NSFetchedResultsController
id <NSFechedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections]
objectAtIndex: section];
NSInteger rows = [sectionInfo numberOfObjects];
return rows + insertRows;
}
Then in the delegate tableView:numberOfRowsInSection:, I add accessory to the inserted row to mark it as Add.
The lesson I learned from this experience is that when a row is dynamically added to the table view, an entity object is not needed to be created in the managed object context. The object is created only on the event to commit the edit style (add). Another important thing to remember is that I have to track the rows in section in sync with the dynamically inserted or removed rows, as described above.
By the way, the reason I tried to add a row to my table view as an UI to add new entity or data is based on iPhone's Contact application. I understand that the most common way to add new entity is to display Add button on the navigation bar, but Contact app provides an alternative way to do that. If you select one person and touch the edit button on navigation bar, several Add rows are displayed in the table view in an animation way. I am not sure if my solution is the correct way to achieve this goal. Please correct me and I would like to accept any good answers!