iphone uitableview grouped by date implementation - iphone

So currently I have a table that displays messages, but I'd like to group the messages by date, e.g.
Tuesday 4/5/98 //header
message 1
message 2
message 3
Wednesday 4/6/98
message 1
etc.
So right now, it's just one long NSMutableArray oldArr (sorted).
What I was thinking of doing, was creating another NSMutableArray (groupArr) of unique date objects (DateGroup), in which each DateGroup would have the following ivars:
int size; //number of messages for date
int index; //index of first message in the total array
//so I can easily retrieve the object when the section and row is asked
NSDate date; //need the date for the header
With these ivars, I can get all the sections with groupArr size, all the individual row sizes by accessing the DateGroup size, and the individual cell when given a section, row arguments by getting the index + row.
I think this is the best way to do it. However, I am having problems populating the groupArr from the oldArr (which will dynamically increase in size). I was thinking of going one by one through the oldArr with this psuedocode:
NSDate date = nil;
int size = 1;
int index = 0;
for (int i = 0; i < oldArr.size; i++) {
OldGroup* cur = [oldArr objectAt:i];
if (date is different from cur->date){ //i know, it's pseudocode
DateGroup* newGroup = [[DateGroup alloc] initWithDate:cur->date index:index];
[groupArr add:NewGroup];
date = cur->date;
index += size;
size = 1; <br/>
} else{ //the date is the same, so the object belongs in the group
[groupArr lastObject].size++;
}
}
Anyway, while I think this will work, it seems very unelegant to me. I was thinking about using the "indexOfObjectPassingTest" of NSMutableArry to find the next date, but can't seem to implement it conceptually. I'm trying to design a good way to do this. Any suggestions?

I haven't even compiled this so beware of errors and leaks, and it is perhaps not the most efficient way. But I think you can use the NSDateFormatter to chop the time component that you don't care about when sorting dates into groups. The result is an array of groups with each group being an array of your events, sorted into dates. Then you just need a sort predicate to arrange it in the order you choose to display it and it's ready for the UITableView.
- (void)addDatedEvent:(MyEventClass*)newEvent ToGroup:(NSMutableArray*)dateGroups
{
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
NSDate* eventDate = newEvent.evDate;
NSString* dateString = [dateFormatter stringFromDate:eventDate];
BOOL added = NO;
for(NSMutableArray* group in dateGroups)
{
MyEventlass* firstEvent = [group objectAtIndex:0];
NSDate* firstEventDate = firstEvent.evDate;
NSString* firstEventDateString = [dateFormatter stringFromDate:firstEventDate];
if([firstEventDateString isEqualToString:dateString])
{
// match - this event joins others in an existing group
[group addObject:newEvent];
added = YES;
}
}
if(added == NO)
{
// need to create a new group since this is the first date
NSMutableArray* newGroupArray = [NSMutableArray arrayWithObject:newEvent];
[dateGroups addObject:newGroupArray];
}
}

Related

Performance issue creating Section Index Titles for UITableView

I'm displaying an array of contacts ( [[ContactStore sharedStore]allContacts] ) in a tableview and have divided the list into alphabetic sections. I have used the following code to return an array of the first letters of the contacts, and a dictionary of the number of entries per letter.
//create an array of the first letters of the names in the sharedStore
nameIndex = [[NSMutableArray alloc] init];
//create a dictionary to save the number of names for each first letter
nameIndexCount = [[NSMutableDictionary alloc]init];
for (int i=0; i<[[[ContactStore sharedStore]allContacts]count]; i++){
//Get the first letter and the name of each person
Contact *p = [[[ContactStore sharedStore]allContacts]objectAtIndex:i];
NSString *lastName = [p lastName];
NSString *alphabet = [lastName substringToIndex:1];
//If that letter is absent from the dictionary then add it and set its value as 1
if ([nameIndexCount objectForKey:alphabet] == nil) {
[nameIndex addObject:alphabet];
[nameIndexCount setValue:#"1" forKey:alphabet];
//If its already present add one to its value
} else {
NSString *newValue = [NSString stringWithFormat:#"%d", ([[nameIndexCount valueForKey:alphabet] intValue] + 1)];
[nameIndexCount setValue:newValue forKey:alphabet];
}
}
This works, however it is very slow when the array is large, I'm sure there's a better way to do this but I'm quite new to this so am not sure how. Are there any suggestions for a better way to do this?
Although Bio Cho has a good point, you might see an increase in performance by calling
[[ContactStore sharedStore]allContacts]
only once. For example:
nameIndex = [[NSMutableArray alloc] init];
nameIndexCount = [[NSMutableDictionary alloc] init];
/*
Create our own copy of the contacts only once and reuse it
*/
NSArray* allContacts = [[ContactStore sharedStore] allContacts];
for (int i=0; i<[allContacts count]; i++){
//Get the first letter and the name of each person
Contact *p = allContacts[i];
NSString *lastName = [p lastName];
NSString *alphabet = [lastName substringToIndex:1];
//If that letter is absent from the dictionary then add it and set its value as 1
if ([nameIndexCount objectForKey:alphabet] == nil) {
[nameIndex addObject:alphabet];
[nameIndexCount setValue:#"1" forKey:alphabet];
//If its already present add one to its value
} else {
NSString *newValue = [NSString stringWithFormat:#"%d", ([[nameIndexCount
valueForKey:alphabet] intValue] + 1)];
[nameIndexCount setValue:newValue forKey:alphabet];
}
}
Though I can't say for sure, I'd guess that repeatedly accessing your shared store is what's killing you. Maybe only accessing it once will give you what you need.
Consider storing your contacts in Core Data and using an NSFetchedResultsController.
The NSFetchedResultsController will only load a subset of the rows which are visible on the table view, thus preventing your user from having to wait for all the contacts to be sorted.
NSFetchedResultsController will also sort your contacts by an attribute (ie. first or last name), and you can set your section titles to be the first letter of the field you're sorting by.
Take a look at this question and this tutorial.

sorting NSMutableArray contain NSArray using sortedArrayUsingComparator

I got problem for sorting array of nsdate.
so i have array of date inside nsmutablearray
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd"];
NSArray *arrString = [[NSArray alloc] initWithObjects:#"2012-05-01", #"2012-02-01", #"2012-22-03", #"2012-05-01", #"2012-15-01", nil];
NSMutableArray *arrDate = [[NSMutableArray alloc] initWithCapacity:arrString.count];
for (NSString *item in arrString) {
NSDate *date = [dateFormatter dateFromString:item];
[arrDate addObject:date];
}
as i saw in many cases they use code like this to sort array:
NSArray *sortedArray = [arrDate sortedArrayUsingComparator:^(id firstObject, id secondObject) {
return [((NSDate *)firstObject) compare:((NSDate *)secondObject)];
}];
i tried to put my array on it, but not worked.
Anyone can help me how actully best way to sort nsmutablearray contain nsdate. I want to put the latest date at top of the index array.
So the real question is, how can i sort data inside arrDate?
Thank you.
Your date format does not match the date strings you are using.
yyyy-MM-dd hh:mm:ss
means the dates you supply should look something like:
2012-05-09 15:54:21
^^ Day
^^ Month
i.e. your strings are missing the hours, minutes and seconds and you have some dates with month 22 and month 15. If a date formatter can't parse a date, I think it returns nil which would mean you are trying to put nil into an array, which will cause an exception.
Edit
To actually sort the dates, your sample code looks OK except that you want most recent first, so reverse the compare:
NSArray *sortedArray = [arrDate sortedArrayUsingComparator:^(id firstObject, id secondObject) {
return [secondObject compare: firstObject];
}];
Or with a mutable array, you can sort in place:
[arrDate sortUsingComparator:^(id firstObject, id secondObject) {
return [secondObject compare: firstObject];
}];

Extracting specific data from a Mutable Dictionary

I have a Mutable Dictionary which allows people to choose a selection of days of the week. Once a day has been selected the state is updated using numberWithBool.
When I NSLog the output it looks something like this:
{
day = Monday;
isSelected = 1;
},
{
day = Tuesday;
isSelected = 0;
},
{
day = Wednesday;
isSelected = 0;
},
{
day = Thursday;
isSelected = 0;
},
{
day = Friday;
isSelected = 0;
},
{
day = Saturday;
isSelected = 0;
},
{
day = Sunday;
isSelected = 1;
}
I would like to be able to extract the chosen days and produce the output in the form of a string. So in this example the output would be: Monday, Sunday
How can I do this?
My code for creating the dictionary is below:
NSMutableArray * tempSource = [[NSMutableArray alloc] init];
NSArray *daysOfWeek = [NSArray arrayWithObjects:#"Monday", #"Tuesday", #"Wednesday", #"Thursday", #"Friday", #"Saturday", #"Sunday",nil];
for (int i = 0; i < 7; i++)
{
NSString *dayOfWeek = [daysOfWeek objectAtIndex:i];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys:dayOfWeek, #"day", [NSNumber numberWithBool:NO], #"isSelected",nil];
[tempSource addObject:dict];
}
[self setSourceArray:tempSource];
[tempSource release];
You can loop thru all of your items in the array and build a side-array only containing names of the day (1), or you can use a predicate and then KVC to extract the days directly (2).
Then join the components of the filtered array into a string.
Solution 1:
NSMutableArray selectedDays = [[NSMutableArray alloc] init];
for(NSDictionary* entry in sourceArray)
{
if (([entry objectForKey:#"isSelected"] boolValue]) {
[selectedDays addObject:[entry objectForKey:#"day"]];
}
}
NSString days = [selectedDays componentsJoinedByString:#", "];
[selectedDays release];
Solution 2:
NSPredicate* filter = [NSPredicate predicateWithFormat:#"SELF.isSelected == 1"]; // not sure about the exact format (no mac here to test right now so you may adapt a bit if it does not work directly)
// get the array of dictionaries but only the ones that have isSelected==1
NSArray selectedEntries = [sourceArray filteredArrayUsingPredicate:filter];
NSArray selectedDays = [selectedEntries valueForKey:#"day"]; // extract the "days" keys of the dictionaries. We have a NSArray of strings then.
NSString days = [selectedDays componentsJoinedByString:#", "];
As a side note, your way of doing this is quite strange. Why having an NSArray of NSDictionaries for this? As this is simple and a static-size array containing only BOOL, you may instead for this particular case simply use C array BOOL selected[7] and nothing more.
Then to have the name of the weekdays you should instead use the methods of NSCalendar/NSDateFormatter/NSDateComponents to get the standard names of the weekdays (automatically in the right language/locale of the user): create an NSDate using a NSDateComponent for which you simply define the weekday component, then use an NSDateFormatter to convert this to a string, choosing a string format that only display the weekday name.
-(void)tableView:(UITableView*)tv didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
// selectedDays is an instance variable in .h declared as
// BOOL selectedDays[7];
selectedDays[indexPath.row] = ! selectedDays[indexPath.row];
[tv reloadData];
}
-(UITableViewCell*)tableView:(UITableView*)tv cellForRowAtIndexPath:(NSIndexPath*)indexPath {
static NSString* kCellIdentifier = #"DayCell";
UITableViewCell* cell = [tv dequeueReusableCellWithIdentifier:kCellIdentifier];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:... identifier:kCellIdentifier]; autorelease];
// configure here every property that is common to all for your cells (text color, etc)
}
// configure here things that will change from cell to cell
cell.accessoryType = selectedDays[indexPath.row] ? UITableViewCellAccessoryTypeCheckmarck : UITableViewCellAccessoryTypeNone;
cell.textLabel.text = weekdayName(indexPath.row);
return cell;
}
// Simple C function to return the name of a given weekday
NSString* weekdayName(int weekday)
{
#if WAY_1
/**** Solution 1 ****/
// Optimization note: You may compute this once for all instead of recomputing it each time
NSDateComponents* comp = [[[NSDateComponents alloc] init] autorelease];
[comp setWeekday:weekday+1]; // weekdays start at 1 for NSDateComponents
NSDate* d = [[NSCalendar currentCalendar] dateFromComponents:comp];
NSDateFormatter* df = [[[NSDateFormatter alloc] init] autorelease];
[df setDateFormat:#"EEEE"]; // format for the weekday
return [[df stringFromDate:d] capitalizedString];
#else
/**** Solution 2 ****/
NSDateFormatter* df = [[[NSDateFormatter alloc] init] autorelease];
NSArray* weekdayNames = [df weekdaySymbols]; // or maybe one of its sibling methods? check what is returned here to be sure
return [weekdayNames objectAtIndex:weekday];
#endif
}
Is there a reason that you avoid NSMutableIndexSet?
It may simplify your code to:
NSArray *daysOfWeek = ...;
NSMutableIndexSet *indexesOfSelectedDays = ...;
NSArray *selectedDays = [daysOfWeek objectsAtIndexes:indexesOfSelectedDays];
Use NSPredicate to create a predicate that selects the items in the array where isSelected is true, and use that to filter the array using NSArray's -filteredArrayUsingPredicate: method. (#Miraaj posted a good example of using a predicate faster than I could type it.) Then take that filtered array and pick out the days, like this:
NSArray *selectedItems = [sourceArray filteredArrayUsingPredicate:somePredicate];
NSArray *days = [selectedItems valueForKey:#"day"];
NSString *daysString = [days componentsJoinedByString#", "];

Change label text with value from different file?

I have these functions here that will record the time between starting point and pressing a button to stop the time, then finally print the length of time recorded:
-(void)informToPress
{
textLabel.text = #"Test, press the button";
//begin record and end record on button press
startDate = [[NSDate alloc]init];
}
-(IBAction)stopTime{
stopDate = [[NSDate alloc]init];
textLabel.text = [NSString stringWithFormat:#"Time : %f", [stopTimer timeIntervalSinceDate:startTimer]];
}
But where I have:
textLabel.text = [NSString stringWithFormat:#"Time : %f", [stopTimer timeIntervalSinceDate:startTimer]];
I need this to be placed in a different View, and therefore a different .m file! How could I use this line of code in a completely different file? As the new file/view doesn't know what the values are or textLabel.
You can implement a singleton called dateManager with the properties startTimer and stopTimer.
You will create only one instance of dateManager so it will be the same value for your properties anywhere in the code.
Or you can also create in one .h file (it's easier):
static NSDate *startTimer = nil;
static NSDate *stopTimer = nil;
And give them values when you need but don't forget to include the .h file where you need the variables.
And the right way to initiate a NSDate with the current date is to do like this :
NSDate *currentDate = [NSDate date];

Date Sectioned table?

I have a plist which contains an array of NSDictionary 's which represent a certain event each dictionary contains a few pieces of information about the event and a NSDate with the date of the event e.g.
I wish to create a sectioned table view with this date very much like in the Calendar app which ships with the iPhone when you click on the "List" view. You can see there are only sections for the dates where there are events.
So whats the best way to find out to begin with how many NSDictionary 's have the same date (so I know how many sections to create and how many rows in each section, as each section will have a different amount or rows).
Thanks
I did something very similar to this for Reconnected except my sections are for years (see the History screenshot).
Sort the array by the date key.
Start with the first item. The date of the first item represents the first segment. In my case, I only care about the year.
Create an array for the section with the date. Add the currently inspected to the section's array. Add the section's array to another array that will be the array of all sections in your table.
Move on to the next item. If the next item's date equals the previous item's date, add the current item to the current section's array. Otherwise, apply step #3 to the new date.
Repeat the previous step for rest of the array from your plist.
At the end of step 5, you should have an array of sections. From that section you can send it a message for the number of NSDictionary's you've added to the section which will represent each rows in your table.
After a bit of playing around this is what I have come up with, at the moment its just a foundation tool to keep it clear.
#import <Foundation/Foundation.h>
NSDate* normalizedDateWithDate(NSDate *date) {
NSCalendar *calendar = [NSCalendar currentCalendar];
unsigned unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDateComponents *comp = [calendar components:unitFlags fromDate:date];
return [calendar dateFromComponents:comp];
}
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString *plistPath = #"flights.plist";
NSArray *array = [[NSArray alloc] initWithContentsOfFile:plistPath];
NSMutableSet *flightDates = [[NSMutableSet alloc] init];
for (NSDictionary *oneFlight in array)
[flightDates addObject:normalizedDateWithDate([oneFlight objectForKey:#"flightDate"])];
NSLog(#"Number of Sections Required: %d", [flightDates count]);
NSMutableDictionary *datesAndFlights = [[NSMutableDictionary alloc] init];
for (NSDate *fDate in flightDates) {
NSMutableArray *sectionFlights = [[NSMutableArray alloc] init];
for (NSDictionary *oneFlight in array) {
if ([normalizedDateWithDate([oneFlight objectForKey:#"flightDate"]) isEqualToDate: normalizedDateWithDate(fDate)])
{
[sectionFlights addObject:oneFlight];
}
}
[datesAndFlights setObject:sectionFlights forKey:normalizedDateWithDate(fDate)];
[sectionFlights release];
}
NSEnumerator *enumerator = [datesAndFlights keyEnumerator];
NSDate *key;
while ((key = [enumerator nextObject])) {
NSLog(#"Key: %#", key);
for (NSDictionary *oneFlight in [datesAndFlights objectForKey:key]) {
NSLog(#"flightNumber: %# and Total Time: %#", [oneFlight objectForKey:#"flightNumber"], [oneFlight objectForKey:#"totalTime"]);
}
}
[array release];
[flightDates release];
[datesAndFlights release];
[pool drain];
return 0;
}
This is just what I have managed to put together and it seems to work but if anyone can see a way to make this better or more concise please say so! Also the function at the top which I use to make sure the date is always at time 00:00:00 when I compare it I have seen the NSCalendar – rangeOfUnit:startDate:interval:forDate: method in the documentation does anyone know if its better to use this instead?
Thanks