Sorting custom objects in UITableView section - ios

I'm making an app whereas I have created a custom class which inherits from NSObject. This class contains various properties whereas one of them is a NSDate property and another one is a NSString. I would like to sort these objects in a UITableView using sections whereas each section represents the month and year of the NSDate in the object. So far, I've only managed to populate the list using a detail UITableViewCell to show the information but I'm not sure how to go about using sections as they're dynamic and not static cells.
I kind of brainstormed solutions and one of them would be to create a for-loop for all the objects and count the number of months and return that number in the numberOfSectionsInTableView: method - but I'm far from certain if this is the best and most proper way of addressing it.
Could someone help me with this?
My custom objects contain various properties but these are the ones we need to focus on:
#property (nonatomic, strong) NSString *information;
#property (nonatomic, strong) NSDate *dateAdded;
Thank you!
Erik

So give your objects a yearAndMonth property. in the getter for the property, check the instance variable. If it's !0, return it. If it ==0, use NSCalendar to calculate the year and month, turn it into a number (year*100+month), save it in the ivar, and return it.
(Also make the setter on the date property zero out the yearAndMonth property.)
Now you can write code that breaks your table view into sections based on the value of this property. For any given object, it will only be calculated once, so it shouldn't impact performance over-much.

I think a lot of it is a matter of preference. In the past for something like this I would just create a two demential array. Where the array in the array would represent the section and when you need need a section header you pull that section array out and populate the header based on the first object in that array.
NSArray *sectionArray = [self arrayAfterSorting:customObjectArray];
//section would be something like this
NSArray *section = sectionArray[section];
return section.count;
//header
NSArray *section = sectionArray[section];
CustomObject *customObject = section[0];
return customObject.whateverHeaderShouldBeBasedOnObject;
I personally try to avoid having two separate arrays and prefer to have everything built into one two dimensional array. Hope that makes sense and helps.
Edit:
If the issue is figuring out how to sort I would look at this question and answer. Although they are talking about putting it into a dictionary I am sure you can apply the same logic to an array.
Sort NSArray with NSDate object into NSDictionary by month
Hope that helps.

is your Array which contains your custom Objects sorted? Probably not i guess. I'd first start with that:
NSArray *sortedObjectArray = [youArray sortedArrayUsingComparator:^NSComparisonResult(YourObjectClass *a, YourObjectClass *b) {
return [a.dateAdded compare:b.dateAdded];}];
As you now got that, it would be the best to actually find out how many table view sections you need. To loop through your sortedArray would be the best approach there. But keep in mind to do it e.g. in viewDidLoad or some place else, where you retrieve your data, to make sure that you actually perform your search and data aggregation only once.
Doing that in numberOfSectionsInTableView would cause your app to repeat all this unnecessary calculation each time the tableView is reloaded.
So, what about the sections right? I'd recommend an NSMutableArray which represent the sections you need. This object then should contain NSMutableArrays itself, which contain your custom Objects.
But first we need to find out how many section you will actually need.Just do a for loop like this:
NSMutableArray *dateArray = [[NSMutableArray alloc] init];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDate *recentDate;
for (YourObjectClass *object in sortedObjectArray) {
if (!recentDate) {
[dateArray addObject:object.dateAdded];
recentDate = object.dateAdded;
}
else {
NSDateComponents *currentDateComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitYear fromDate:object.dateAdded];
NSDateComponents *recentDateComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitYear fromDate:recentDate];
if (recentDateComponents.year != currentDateComponents.year || recentDateComponents.month != currentDateComponents.month) {
[dateArray addObject:object.dateAdded];
recentDate = object.dateAdded;
}
}
}
So now you got your dateArray which contains all distinct dates in terms of month and year.
NSMutableArray *finalArray = [[NSMutableArray alloc] init];
for (NSDate *date in dateArray) {
NSMutableArray *subArray = [[NSMutableArray alloc] init];
for (YourObjectClass *object in sortedObjectArray) {
NSDateComponents *currentDateComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitYear fromDate:object.dateAdded];
NSDateComponents *recentDateComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitYear fromDate:date];
if (recentDateComponents.year == currentDateComponents.year || recentDateComponents.month == currentDateComponents.month) {
[subArray addObject:object];
}
else {
break;
}
}
[finalArray addObject:subArray];
}
This should be it. Now just tell your tableView Datasource that you have finalArray.count sections. And in numberOfRowsInSection: you just get the subArray on the sectionIndex and return its count value. Hope this helps you out :)
(Didn't test the code, so be aware :))

I managed to put this code together that works, although I realised I might want to change the UI as the data wasn't presented as well as I initially thought. This is the code I made, all the other answers helped me with this!
// Create a sections NSMutableArray
_sectionsArray = [[NSMutableArray alloc] init];
// Cycle through the workdays and populate sectionsArray
NSString *currentMonth;
NSMutableArray *currentMonthArray = [[NSMutableArray alloc] init];
for (DayModel *currentModel in sortedWorkDays)
{
// Create a currentMonthArray and initialize it
// Initial currentMonth value
if (currentMonth == nil)
{
currentMonth = [self monthFromDate:currentModel.registerDate];
}
if ([currentMonth isEqualToString:[self monthFromDate:currentModel.registerDate]])
{
NSLog(#"current");
// Current month
[currentMonthArray addObject:currentModel];
if (([sortedWorkDays indexOfObject:currentModel] + 1) == [sortedWorkDays count])
{
// Complete
NSLog(#"LAST OF ALL");
[_sectionsArray addObject:currentMonthArray];
currentMonthArray = nil;
currentMonthArray = [[NSMutableArray alloc] init];
currentMonth = [self monthFromDate:currentModel.registerDate];
[currentMonthArray addObject:currentModel];
}
} else
{
// Finished with this month
NSLog(#"LAST");
[_sectionsArray addObject:currentMonthArray];
currentMonthArray = nil;
currentMonthArray = [[NSMutableArray alloc] init];
currentMonth = [self monthFromDate:currentModel.registerDate];
[currentMonthArray addObject:currentModel];
}
}

Related

Load UITableView sections by NSDate from NSDictionary?

I have an NSArray of NSDictionaries in my app. In each dictionary I hold an NSDate called "RunDate." The problem I am having now is that the code I am trying to do it with is very inefficient. Basically I only want one section per date out of all the dictionaries. Then in each section (sorted by that date), I would load the appropriate dictionary that had that date.
In the code below I made a new NSArray of NSDictionarys which held a date and number of that date (so I could know how many rows are in each section). The problem is, this code looks and feels very inefficient and I was wondering if there were any ways my code is incorrect or could be improved upon. There can be over 500 entries and the code I have now would be very slow. Does anyone have any suggestions on it?
runArray = [[NSMutableArray alloc] init];
runArray = [[[NSUserDefaults standardUserDefaults] arrayForKey:#"RunArray"] mutableCopy];
runDictTableArray = [[NSMutableArray alloc] init];
for (NSDictionary *dict in runArray) {
NSDictionary *runInfoDict = [[NSMutableDictionary alloc] init];
NSDate *theDate = [dict objectForKey:#"RunDate"];
//Check if we already have this date in the saved run array
BOOL goToNextDict = FALSE;
for (NSDictionary *savedDict in runDictTableArray) {
if ([theDate compare:[savedDict objectForKey:#"RunDate"]] == NSOrderedSame) {
goToNextDict = TRUE;
break;
}
}
if (goToNextDict)
continue;
////////////////////////////
//Now we check how many more we have of this date
int numbOfDate = 1;
int index = (int)[runArray indexOfObject:dict];
for (int i = index; i < [runArray count]; i++) {
NSDictionary *dictInner = [runArray objectAtIndex:i];
if ([theDate compare:[dictInner objectForKey:#"RunDate"]] == NSOrderedSame) {
numbOfDate++;
}
}
////////////////////////////
[runInfoDict setValue:[NSNumber numberWithInt:numbOfDate] forKey:#"DateAmount"];
[runInfoDict setValue:theDate forKey:#"Date"];
[runDictTableArray addObject:runInfoDict];
}
Some suggestions:
You probably only need 1 NSMutableDictionary, rather than a NSMutableArray of NSDictionary. While looping through runArray, check if your dictionary has a value for your date (objectForKey returns a value). If it does, add 1 to the count. If it does not, add that date as a key to the dictionary with a value of 1. This way, you won't have to do the inner loop to get the number of times a date occurs. You won't need the 'go to next dictionary' logic either, I would think.
runArray = [[NSMutableArray alloc] init]; doesn't really do anything since you're immediately re-assigning runArray.
Consider using NSInteger over regular int, NSInteger will give you the appropriate size for whatever architecture your app is running on.
There's some cool syntax shortcuts you might like. You can avoid [runInfoDict setValue:[NSNumber numberWithInt:numbOfDate]... by simply writing [runInfoDict setValue:#(numbOfDate) ..., which will put the value into NSNumber for you.

Modifying sort key in awakeFromFetch doesn't update sort order

I have a Core Data object that has a property "completed" and also a time to reset this value:
#property (nonatomic) BOOL completed;
#property (nonatomic, retain) NSDate * next_reset;
I fetch these objects with a NSFetchedResultsController, sorting on the "completed" key:
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"completed" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,nil];
The wrinkle: I reset "completed" on fetch:
- (void)awakeFromFetch
{
[super awakeFromFetch];
if (!self.auto_reset || !self.completed)
{
return;
}
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
if ([now compare:self.next_reset] == NSOrderedDescending)
{
self.last_reset = now;
self.completed = NO;
}
}
My problem is that this modification is not reflected in the sorting of the fetched results - I get completed items mixed in with uncompleted ones. When I complete an item "live" the sorting does update as I expect.
According to the docs, in awakeFromFetch, Core Data is not monitoring changes made to the object. I do see that even a [self.managedObjectContext save:nil]; does not save my changes made here. I've tried calling -willChangeValueForKey/-didChangeValueForKey before I modify "completed" but this doesn't change things.
Where can I do these kinds of updates to the object so that Core Data properly sees them? Obviously I don't want to do it anywhere but the model (certainly not in my controller that is fetching these objects!) but I don't see any other places to put this update code.
I at least can get this to sort of work with this code:
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval interval_to_next_reset = [self.next_reset timeIntervalSinceDate:now];
if (interval_to_next_reset < 0.01)
{
interval_to_next_reset = 0.01;
}
[self performSelector:#selector(resetCompleted) withObject:nil afterDelay:interval_to_next_reset];
}
- (void)resetCompleted
{
self.last_reset = [NSDate dateWithTimeIntervalSinceNow:0];
self.completed = NO;
}
but this causes the table view to visibly rearrange itself immediately after being displayed, which isn't ideal.
I ended up solving this problem by not doing it this way.
Instead of writing this code in -awakeFromFetch (which clearly isn't meant to be used this way) I added code in my app delegate's -didBecomeActive (& friends) to call a class method +processQuestsNeedingReset which updates all of these at once. That update is reflected in the table view.

UITableView Section Headers by month of dates in array

I have an NSArray with something similar to:
6/1/13 | Data
6/2/13 | Data
7/1/13 | Data
9/1/13 | Data
What I need to somehow get the months to create section headers - but only if they are in the array and then break the dates up into the appropriate sections. Looking like:
(Section Header)June 2013
6/1/13 | Data
6/2/13 | Data
(Section Header)July 2013
7/1/13 | Data
(skips august as no dates from august are in array)
(Section Header)September 2013
9/1/13 | Data
I am attempting to implement:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return #"June 2013";
}
But obviously need this to dynamically update with whatever months are in the array. The dates are actually NSDates that are in the array - if that makes any difference.
I have cobbled together something that should at least compile, but which is completely untested. Basically this involves pre-processing your array and storing the results in other collections that can then serve as model objects for your UITableViewDataSource.
Add these properties to the class that is your data source. You have to declare them differently if you are using ARC.
#property(retain) NSMutableArray* tableViewSections;
#property(retain) NSMutableDictionary* tableViewCells;
Add this method to your data source and make sure that you invoke it at some time before UITableView invokes your first data source method. Important: Your array must contain the NSDate objects in sorted order (the example in your question implies that this is the case).
- (void) setupDataSource:(NSArray*)sortedDateArray
{
self.tableViewSections = [NSMutableArray arrayWithCapacity:0];
self.tableViewCells = [NSMutableDictionary dictionaryWithCapacity:0];
NSCalendar* calendar = [NSCalendar currentCalendar];
NSDateFormatter* dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
dateFormatter.locale = [NSLocale currentLocale];
dateFormatter.timeZone = calendar.timeZone;
[dateFormatter setDateFormat:#"MMMM YYYY"];
NSUInteger dateComponents = NSYearCalendarUnit | NSMonthCalendarUnit;
NSInteger previousYear = -1;
NSInteger previousMonth = -1;
NSMutableArray* tableViewCellsForSection = nil;
for (NSDate* date in sortedDateArray)
{
NSDateComponents* components = [calendar components:dateComponents fromDate:date];
NSInteger year = [components year];
NSInteger month = [components month];
if (year != previousYear || month != previousMonth)
{
NSString* sectionHeading = [dateFormatter stringFromDate:date];
[self.tableViewSections addObject:sectionHeading];
tableViewCellsForSection = [NSMutableArray arrayWithCapacity:0];
[self.tableViewCells setObject:tableViewCellsForSection forKey:sectionHeading];
previousYear = year;
previousMonth = month;
}
[tableViewCellsForSection addObject:date];
}
}
Now in your data source methods you can say:
- (NSInteger) numberOfSectionsInTableView:(UITableView*)tableView
{
return self.tableViewSections.count;
}
- (NSInteger) tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
id key = [self.tableViewSections objectAtIndex:section];
NSArray* tableViewCellsForSection = [self.tableViewCells objectForKey:key];
return tableViewCellsForSection.count;
}
- (NSString*) tableView:(UITableView*)tableView titleForHeaderInSection:(NSInteger)section
{
return [self.tableViewSections objectAtIndex:section];
}
[...]
The rest of the implementation is left as an exercise to you :-) Whenever the content of your array changes you obviously need to invoke setupDataSource: to update the contents of tableViewSections and tableViewCells.
You need to convert your existing single array and create a new array of dictionaries. Each dictionary in this new array will contain two entries - one for the month and the other entry will be an array containing the data for each row associated with the month.
If you need to add a new row to this structure, see of the month is already in the list. If so, update that month's array. Otherwise create a new dictionary with the new month and a new array containing the one new row.

iPhone Core Data how to add date sections to events with timestamps?

I have two types of Events stored in my core data stack, each one having a timestamp. I'm interested if there's a good way to display these records in a UITableView with sections, where each section is arbitrary long (a day, week, etc).
Is there a way to convert a timestamp of a core data object into a section title, rounding down hours of the day?
So we would get:
October 5 < section title
Record 1 < records displayed in the section
Record 2
Record 3
October 6
Record 4
October 7
Record 5
...
-OR-
Week 1
Record 1
Record 2
Week 2
Record 3
...
Here's what I'm currently using to accomplish this goal, but it is limited to each section being a day.
But lets say that I have not thought about this requirement and have a list of events with just timestamps. How can I break them up into sections?
//the method used to convert a date into a number to store with the event
-(int)getDateIDFromDate:(NSDate*)date
{
int gmtOffset = [[NSTimeZone localTimeZone] secondsFromGMT];
int dateID =([date timeIntervalSinceReferenceDate]+gmtOffset)/86400;
return dateID;
}
//when inserting a record, the number is saved
newManagedObject.dayID = [NSNumber numberWithInt:[self getDateIDFromDate:date]];
//when retrieving, the number is used as a section key path
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"dayID" cacheName:#"Day"];
//the number gets converted back into the date.
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
// Display the authors' names as section headings.
// return [[[dataManager.dreamEventsController sections] objectAtIndex:section] name];
NSString* dayIndex = [[[dataManager.fetchedResultsController sections] objectAtIndex:section] name];
int dayFromReferenceDate = dayIndex.intValue;
return [dataManager.sectionDateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:(dayFromReferenceDate+1)*86400]];
}
Adding (redundant) data just for displaying purposes should always be a last resort.
In slightly similar cases, I just add a category to the CoreData object, e.g.
-(NSString*)firstLetter
{
NSString *title = self.title;
if ( [title length] == 0 ) return #"?";
return [title substringToIndex:1];
}
then I just use this as the sectionNameKeyPath and all else is just the same as in a normal situation.
In your case, this category would be a little more elaborate, but the general outline could be the same. And the fact that section names will be volatile, should also be kept in mind.
A tricky (/ugly) part will be to communicate the current sectioning setting to the category. Some global or static variable could do the job efficiently.
The best way is to add a transient property to your managed object model. In that property's accessor, return a normalized NSDate with truncated hours (You can do this with NSDateComponents). Then when it's time to fetch those objects, set .. sectionNameKeyPath: to that transient property.
Updated: Let's say your NSManagedObject subclass had a transient attribute monthOfTheYear:
- (NSNumber*)monthOfTheYear
{
[self willAccessValueForKey:#"monthOfTheYear"];
NSDateComponent *dateComponent = [cachedCalendar components:NSMonthCalendarUnit fromDate:self.timestamp]; // cachedCalendar is a NSCalendar instance
[self didAccessValueForKey:#"monthOfTheYear"];
return [NSNumber numberWithInteger:dateComponent.month]; // or a normalized number that takes consideration of the year too
}
We don't create an NSString transient attribute directly because that will mess up your sorting (and you lose multi-language support).
The willAccessValueForKey: and didAccessValueForKey: are important. You should read more on their documentations.
Then when it's time to display the section titles:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSInteger monthNumber = // Get the monthOfTheYear value from the first NSManagedObject in this section.
return [[cachedDateFormatter monthSymbols] objectAtIndex:(monthNumber-1)]; // cachedDateFormatter is a NSDateFormatter instance
}
Sup bro! my advice would be to use NSDateFormatter
This is the link for Apple documentation: NSDateFormatter
I would add a category on the managedObject where you have the timeStamp,
#interface MyManagedobject (ReadableTimestamp)
-(NSString *)formatedStringFromTimestamp;
#end
#implementation MyManagedobject (ReadableTimestamp)
-(NSString *)formatedStringFromTimestamp{
//TODO: Here you apply all your fancy format, I'm just using the default
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[formatter setDateStyle:dateStyle];
[formatter setTimeStyle:timeStyle];
NSString *result = [formatter stringForObjectValue:self.timestamp];
return result;
}
#end
I hope this helps, good times.
If you're some kind of an optimization freak (like myself) you can define your timeFormatter as an static so that you don't need to build it everytime.

iOS - UITableView Sections based on date ranges

I've got an issue and I'm wondering if it's possible to solve. I have a UITableView that uses Core Data and NSFetchedResultsController. A big part of my app is organizing things based on dates. Ideally, I'd like to divide the TableView into sections based off of 3 date ranges. These ranges would be between now and 2 days, between 2 days and 6 days, and 10 days and beyond. The user has a UIDatePicker, and when they enter a date it would be automatically put into one of these organized sections. Now I know how to easily divide the tableview into sections by each single date, but not how to do it so each section has a time range. Thanks to anyone that might be able to help out.
Just did this myself. The end result is that core data objects are sorted into sections, each one being 1 day wide. Multiple objects may be in one section.
//at a point where you define your fetched results controller
//add #"sectionTitle" as sectionNameKeyPath:
//...
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"sectionTitle" cacheName:#"CacheName"];
//....
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
NSString* dateString = [[[self.sleepQualityController sections] objectAtIndex:section] name];
return dateString;
}
//add this to your managed object's header file:
#property(nonatomic,assign)NSString* sectionTitle;
//this goes into your managed object's implementation file
-(NSDate *)dateWithOutTime:(NSDate *)datDate
{
if( datDate == nil ) {
datDate = [NSDate date];
}
NSDateComponents* comps = [[NSCalendar currentCalendar] components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit fromDate:datDate];
return [[NSCalendar currentCalendar] dateFromComponents:comps];
}
-(NSString*)sectionTitle
{
NSDateFormatter* dateFormaterWeekdayMonthDay = [[NSDateFormatter alloc] init];
dateFormaterWeekdayMonthDay.dateStyle = NSDateFormatterLongStyle;
return [NSString stringWithFormat:#"%#", [dateFormaterWeekdayMonthDay stringFromDate:[self dateWithOutTime:self.date]] ];
}
I would explore using NSExpressions in the fetch request. It can be difficult to find good documentation on using SQL-like expressions with fetch requests; but you can also write your own block for the query to use, which is pretty killer.
Basically what you want is an NSExpression which returns a string for the section name, which you can tell the NSFetchedResultsController to use for the section name key path. You will need to build up an NSExpressionDescription around the expression to add it to the fetch request.
I hope this puts you in the right direction!
FWIW, I would normally be inclined to do an SQL-ish solution (essentially selecting a 'case' expression which compares the date field and selects one of three values), but sometimes with Core Data, it's easier to just pull out the sledge hammer (or rather, it feels like that's what they want us to do).

Resources