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?
Related
I'm having trouble making the sections in a UITableView. I've looked at the documentation for UILocalizedIndexedCollation as well as this sample code project:
https://developer.apple.com/library/ios/#samplecode/TableViewSuite/Listings/3_SimpleIndexedTableView_Classes_RootViewController_m.html#//apple_ref/doc/uid/DTS40007318-3_SimpleIndexedTableView_Classes_RootViewController_m-DontLinkElementID_18
What I have below is basically a straight copy/paste from the sample project. However, the sample project uses a custom object (TimeZoneWrapper.h) and then places the object in the correct section based on the object's instance variable (TimeZoneWrapper.localeName). However, I'm not using custom objects. I'm using just a bunch of regular NSStrings. So my question is what method on NSString should I pass to the #selector() to compare and place the string in the correct section array?
Currently, I'm calling NSString's copy method as a temporary hack to get things working (which it does), but I'm not sure if this is correct. A little explanation would be much appreciated!
- (void)configureSections {
// Get the current collation and keep a reference to it.
self.collation = [UILocalizedIndexedCollation currentCollation];
NSInteger index, sectionTitlesCount = [[collation sectionTitles] count]; // sectionTitles are A, B, C, etc.
NSMutableArray *newSectionsArray = [[NSMutableArray alloc] initWithCapacity:sectionTitlesCount];
// Set up the sections array: elements are mutable arrays that will contain the locations for that section.
for (index = 0; index < sectionTitlesCount; index++) {
NSMutableArray *array = [[NSMutableArray alloc] init];
[newSectionsArray addObject:array];
}
// Segregate the loctions into the appropriate arrays.
for (NSString *location in locationList) {
// Ask the collation which section number the location belongs in, based on its locale name.
NSInteger sectionNumber = [collation sectionForObject:location collationStringSelector:#selector(/* what do I put here? */)];
// Get the array for the section.
NSMutableArray *sectionLocations = [newSectionsArray objectAtIndex:sectionNumber];
// Add the location to the section.
[sectionLocations addObject:location];
}
// Now that all the data's in place, each section array needs to be sorted.
for (index = 0; index < sectionTitlesCount; index++) {
NSMutableArray *locationsArrayForSection = [newSectionsArray objectAtIndex:index];
// If the table view or its contents were editable, you would make a mutable copy here.
NSArray *sortedLocationsArrayForSection = [collation sortedArrayFromArray:locationsArrayForSection collationStringSelector:#selector(/* what do I put here */)];
// Replace the existing array with the sorted array.
[newSectionsArray replaceObjectAtIndex:index withObject:sortedLocationsArrayForSection];
}
self.sectionsArray = newSectionsArray;
}
Thanks in advance!
You should use #selector(self).
Using #selector(copy) will cause memory leaks in your project
I'm implementing a search field that filters a UITableView according to the text the user enters.
The TableView is built from an array that holds NSStrings (the data to display and search) and may contain 6000+ items.
When the user starts the search, I'm implementing the -(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText method.
My code works, however, when the data array is large, it is very slow and creating a really bad user experience (my iPhone 4s get stuck for a good few seconds).
The way I'm implementing the search (in the method mentioned above) is this:
NSMutableArray *discardedItems = [[NSMutableArray alloc] init]; // Items to be removed
searchResultsArray = [[NSMutableArray alloc] initWithArray:containerArray]; // The array that holds all the data
// Search for matching results
for (int i=0; i<[searchResultsArray count]; i++) {
NSString *data = [[containerArray objectAtIndex:i] lowercaseString];
NSRange r = [data rangeOfString:searchText];
if (r.location == NSNotFound) {
// Mark the items to be removed
[discardedItems addObject:[searchResultsArray objectAtIndex:i]];
}
}
// update the display array
[searchResultsArray removeObjectsInArray:discardedItems];
[myTableView reloadData];
I did not think that looping over an array with a few thousand items would cause any issue...
Any suggestion will be appreciated!
UPDATE
I've just realized that what takes most of the time is this:
[searchResultsArray removeObjectsInArray:discardedItems];
Try fast enumeration way, my snippet:
- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{
if(text.length == 0)
{
self.isFiltered = NO;
}
else
{
self.isFiltered = YES;
self.searchArray = [NSMutableArray arrayWithCapacity:self.places.count];
for (PTGPlace* place in self.places)
{
NSRange nameRange = [place.name rangeOfString:text options:NSCaseInsensitiveSearch];
if(nameRange.location != NSNotFound)
{
[self.searchArray addObject:place];
}
}
}
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.isFiltered)
return self.searchArray.count;
else
return self.places.count;
}
In cellForRowAtIndexPath:
PTGPlace *place = nil;
if(self.isFiltered)
place = [self.searchArray objectAtIndex:indexPath.row];
else
place = [self.places objectAtIndex:indexPath.row];
// Configure the cell...
cell.textLabel.text = place.name;
cell.detailTextLabel.text = [place subtitle];
Try this:
for the first three positions, create 26 index sets, each representing the array index of items with that letter (just lower case). That is, say a entry at idx=100 starts with "formula". The index set representing "f" in the first position will contain the index "100". The index set for the second character 'o' will contain the index 100, and the index set for the third character 'r' will contain 100.
When the user types the character 'f', you immediately have the index set of all array items starting with 'f' (and can create a subset of your major array quickly). When an 'o' is typed next, you can find the intersection of indexes in the first match with the second match. Ditto for the third. Now make a subarray of the major array that had the first three indexes match - you can just use the index sets for this.
Using this drastically reduced array, you can now do brute force matching as you were doing originally.
First,I got some products from IAP
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
[productDetailsList addObjectsFromArray: response.products];
[productDisplayTableView reloadData];
}
How to put them in a uitableview sort by product price? thank you.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *GenericTableIdentifier = #"GenericTableIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: GenericTableIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:GenericTableIdentifier] autorelease];
}
SKProduct *thisProduct = [productDetailsList objectAtIndex:row];
NSUInteger row = [indexPath row];
[button setTitle:localizedMoneyString forState:UIControlStateNormal];
[cell.contentView addSubview:button];
return cell;
}
NSArray *products = [response.products sortedArrayUsingComparator:^(id a, id b) {
NSDecimalNumber *first = [(SKProduct*)a price];
NSDecimalNumber *second = [(SKProduct*)b price];
return [first compare:second];
}];
Swift 2.1.1
public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
let unsortedProducts = response.products
let products = unsortedProducts.sort{($0.price.compare($1.price) == NSComparisonResult.OrderedAscending)}
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
} ...
Swift 3, without the need for self variables. Worked for me like a charm to sort by price from low to high:
let validProducts = response.products
var productsArray = [SKProduct]()
for i in 0 ..< validProducts.count {
let product = validProducts[i]
productsArray.append(product)
}
productsArray.sort{(Double(truncating: $0?.price) < Double(truncating: $1?.price))}
EDIT: Updated for Swift 5.
What you want to do is sort the NSArray prior to trying to read it. There are many options for sorting NSArray, most of them involve creating your own sorting method. You do something like:
[productDetailList sortedArrayUsingFunction:intSort context:NULL];
That will use your comparator method that you specify. The comparison method is used to compare two elements at a time and should return NSOrderedAscending if the first element is smaller than the second, NSOrderedDescending if the first element is larger than the second, and NSOrderedSame if the elements are equal. Each time the comparison function is called, it’s passed context as its third argument. This allows the comparison to be based on some outside parameter, such as whether character sorting is case-sensitive or case-insensitive, but this doesn't matter in your case. The function you have to implement would look like this:
NSInteger intSort(id num1, id num2, void *context)
For more information of the above method take a look at the documentation, it give you an example.
So you can sort the array like this or you always have the choice of sorting the array every time you add something. So every time you add an object you do the sorting yourself and make sure to put it in the right location to keep the array always sorted.
Depending on what you want, I would say keeping the array constantly sorted on insertion time is the best option, so you don't have to waste time while building the view by sorting the array. To do it like this would be simple, every time you enter something into the array, you iterate through the array until you find an object with a price larger than the one you want to enter, then you insert the object at the location before that entry.
In swift
var validProducts = response.products
for var i = 0; i < validProducts.count; i++
{
self.product = validProducts[i] as? SKProduct
self.productsArray.append(product!)
println(product!.localizedTitle)
println(product!.localizedDescription)
println(product!.price)
}
self.productsArray.sort{($0.price < $1.price)}
self.products = products!.sorted(by: { (item1, item2) -> Bool in
return item1.price.doubleValue < item2.price.doubleValue
})
I have many different arrays with dictionaries of hotels in a data model class. For example, the array madridHotels would hold dictionaries of hotels in Madrid. To list these hotels in a tableview, I use a method called madridHotelsCount in my dataModel class:
-(int)madridHotelsCount {
return self.madridHotels.count;
}
In a tableview's numberOfRowsInSection method, I would put
- (NSInteger)tableView:(UITableView*)theTableView numberOfRowsInSection:(NSInteger)section {
return [self.dataModel madridHotelsCount];
}
in order to list the hotels in Madrid. This works just fine. But since I have about 20 cities, I have a feeling that having 20 different VC's and XIB's to make city-based tableviews is plain stupid and wasteful.
In my "Select City" tableview, each city has a key called cityString with the appropriate string meant for the "Hotels in City X" tableview. In Madrid's case, for example, the string would be madridHotelsCount. If I'm not mistaken there is a way to use this information when pushing the "Hotels in City X" tableview controller.
What I can't figure out is how to change the string madridHotelsCount in the tableview method with, for example, barcelonaHotelsCount depending on a key in a "Select City" tableview so that I can only use one VC and XIB.
Hope this makes sense...
You need to change it to something like this so you pass the hotel to each view.
-(int)hotelCountForCity:(NSString *)cityName
I would rewrite it so it was more generic as you shouldn't be repeating yourself like that.
although its not the best way, but if all that code is already there, you can..
NSString city = #"madrid";
SEL selector = selectorFromString([NSString stringWithFormat:#"%#HotelsCount", city]);
int count = 0;
if([self respondsToSelector:selector])
{
int *count = [self performSelector:selector];
}
Change the data structure in dataModel to use
#property (nonatomic, retain) NSDictionary *citiesHotels;
The cities are the keys and the values are the arrays of hotels.
An example might be like
NSArray *madrid = [[NSArray alloc] initWithObjects:#"madridHotelA", #"madridHotelB", nil];
NSArray *barcelona = [[NSArray alloc] initWithObjects:#"barcelonaHotelA", #"barcelonaHotelB", nil];
NSDictionary *citiesHotels = [[NSDictionary alloc] initWithObjectsAndKeys:madrid, #"Madrid", barcelona, #"barcelona", nil];
[madrid release]; madrid = nil;
[barcelona release]; barcelona = nil;
self.citiesHotels = citiesHotels;
[citiesHotels release]; citiesHotels= nil;
Add a method to the model like this
- (NSInteger)hotelCountForCity:(NSString *)city
{
int count = 0;
NSArray *hotels = [self.citiesHotels valueForKey:city];
if (hotels) {
count = [hotels count];
}
return count;
}
To get the hotel you would need something like this in your model
- (Hotel *)hotelAtIndex:(NSInteger)index forCity:(NSString *)city
{
NSArray * hotels = [self.citiesHotels valueForKey:city];
return [hotels objectAtIndex:index];
}
These are called like this
- (NSInteger)tableView:(UITableView*)theTableView numberOfRowsInSection:(NSInteger)section
{
return [self.dataModel hotelCountForCity:theSelectedCityName];
}
And in `cellForRowAtIndexPath you do
Hotel *hotel = [self.dataModel hotelAtIndex:indexPath.row forCity:theSelectedCity];
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;
}