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.
Related
What I'm trying to do is detect which sections of a UITableView are visible, and then change the date of a calendar based on which are visible.
The issue is that there are typically multiple sections viewable at the same time, and I only want to change the date based on the first section index that appears in visibleRowIndexes, not all of them.
Here's my current implementation (Note: I run this function in cellForRowAtIndexPath):
-(BOOL)whatSectionsAreVisible {
NSArray *visibleRowIndexes = [self.agendaTable indexPathsForVisibleRows];
for (NSIndexPath *index in visibleRowIndexes) {
NSNumber *daySection = #(index.section);
static NSDateFormatter *dateFormatter = nil;
if(!dateFormatter){
dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = #"yyyy-MM-dd"; // Read the documentation for dateFormat
}
// Here is where I will map every index.section to an NSDate
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setDay:daySection.intValue]; // <<== Extract int from daySection
[comps setMonth:6];
[comps setYear:2015];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDate *date = [gregorian dateFromComponents:comps];
// Update the day selected according to visible section
[[NSNotificationCenter defaultCenter] postNotificationName:#"kJTCalendarDaySelected" object:date];
// Store currentDateSelected
[self.calendar setCurrentDateSelected:date];
NSLog(#"The visible section has an index of: %ld", (long)index.section);
if (index.section == 0) {
NSLog(#"index.row is indeed 0");
return YES;
}
}
return NO;
}
I tried doing something like NSNumber *daySection = #(index[0].section); instead of NSNumber *daySection = #(index.section);, but that doesn't seem to work.
Don't use a loop if you just want the first visible row. Get the first object of visibleRowIndexes as an NSIndexPath. Then get the section from that.
The indexPathsForVisibleRows method returns the index paths in ascending order. So, get rid of the for loop and execute the code in it only for the first object:
NSArray *visibleRowIndexes = [self.agendaTable indexPathsForVisibleRows];
NSIndexPath *index = [visibleRowIndex objectAtIndex:0];
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];
}
}
I am looking to convert a NSString to an NSDate and would just like to ask a few questions about this.
The purpose of the conversion is to take a section header in a table view which is a string derived from an NSDate, and to pass it back to a UIDatePicker, so that the date picker can use that date in the string.
I've been reading some posts about this and there are tons of formats to work through.
My string date format is in the form:
March 10, 2014 for American formats and 10 March 2014 for UK formats.
What would I need to do to:
1) Get that date to translate over to the UIDatePicker appropriately
2) Work with different country formats so that the date picker is updated for UK and US users.
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"d-MMMM-YYYY"];
NSDate *dateFromString = [[NSDate alloc] init];
dateFromString = [dateFormatter dateFromString:sectionTitle];
dateFromString is currently null.
I've been looking at https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html , Converting NSString to NSDate (and back again) and http://waracle.net/iphone-nsdateformatter-date-formatting-table/ but am not sure how to proceed.
Any assistance would be appreciated on this
I know this isn't directly answering the question, but it makes several of the above comments make more sense:
How to create the data source structure for a table view:
NSMutableArray* sections = [NSMutableArray array];
for (<each section>) {
NSMutableDictionary* section = [NSMutableDictionary dictionary];
[section setObject:sectionTitle forKey:#"title"];
[section setObject:sectionNSDate forKey:#"date"];
[section setObject:otherStuff forKey:#"other_stuff"];
NSMutableArray* rows = [NSMutableArray array];
for (<each row>) {
NSMutableDictionary* row = [NSMutableDictionary dictionary];
[row setObject:rowTitle forKey:#"title"];
[row setObject:image forKey:#"image"];
[rows addObject:row];
}
[section addObject:rows forKey:#"rows"];
[sections addObject:section];
}
It's easy to keep track of & update section and row data, the number of sections and number of rows methods virtually write themselves, you don't need to re-derive data if something is scrolled away and comes back (since you can cache it in the dictionary).
See also
Core Data Sort By Date With FetchedResultsController into sections
Is there a way to group objects from a CoreData entity by date and use them to create multiple sections on a UITableView control?
Essentially, I need something equivalent to the following pseudo-SQL, where my_group_criteria() groups all tests by day:
SELECT * FROM tests GROUP BY my_group_criteria(date)
As an example, the query would group the following rows into 3 separate sections:
[test1 | 2013-03-18 15.30.22]
[test2 | 2013-03-18 14.30.22]
[test3 | 2013-03-18 13.30.22]
[test4 | 2013-03-17 18.30.22]
[test5 | 2013-03-17 19.30.22]
[test6 | 2013-03-15 20.30.22]
As already answered in 2, NSPredicate is not for grouping entities so this may not be the way to go.
Given this, how do I go about creating multiple sections in a UITableView based on some criteria similar to what SQL GROUP BY would do?
I would like to avoid accessing the SQL-lite database directly if at all possible.
Related question 1:
NSPredicate: filtering objects by day of NSDate property
Related question 2:
NSPredicate something equivalent of SQL's GROUP BY
You can modify the DateSectionTitles sample project from the Apple Developer Library according to you needs.
First, you have to modify the accessor function for the transient sectionIdentifier property to create a section identifier based on year+month+day (instead of year+month only):
- (NSString *)sectionIdentifier {
// Create and cache the section identifier on demand.
[self willAccessValueForKey:#"sectionIdentifier"];
NSString *tmp = [self primitiveSectionIdentifier];
[self didAccessValueForKey:#"sectionIdentifier"];
if (!tmp) {
/*
Sections are organized by month and year. Create the section identifier
as a string representing the number (year * 10000 + month * 100 + day);
this way they will be correctly ordered chronologically regardless of
the actual name of the month.
*/
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit)
fromDate:[self timeStamp]];
tmp = [NSString stringWithFormat:#"%d", [components year] * 10000 + [components month] * 100 + [components day]];
[self setPrimitiveSectionIdentifier:tmp];
}
return tmp;
}
Second, the titleForHeaderInSection delegate method must be changed, but the idea is the same: extract year, month and day from the section identifer, and create a header title string from that:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> theSection = [[fetchedResultsController sections] objectAtIndex:section];
NSInteger numericSection = [[theSection name] integerValue];
NSInteger year = numericSection / 10000;
NSInteger month = (numericSection / 100) % 100;
NSInteger day = numericSection % 100;
// Create header title YYYY-MM-DD (as an example):
NSString *titleString = [NSString stringWithFormat:#"%d-%d-%d", year, month, day];
return titleString;
}
Assuming the idea is to group the array of objects into an array of arrays of objects, the approach I would take is this, start by finding the possible values for date (or whatever you want to group by) and then actually sort it out.
NSArray *values = ...;
NSArray *sorting = [values valueForKey:#"#distinctUnionOfObject.propertyToGroupBy"];
NSUInteger sectionCount = [sorting count];
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (int i = 0; i < sectionCount; i++)
[sections addObject:[NSMutableArray array]];
[values enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// Figure out the section for this object
NSUInteger section = [sorting indexOfObject:obj.propertyToGroupBy];
[[sections objectAtIndex:section] addObject:obj];
}];
// sections will contain as many sections as there were unique values
// each of those values is an array containing values that matched the criteria
This can likely be optimised, but from the top of my head, seems to fit the bill.
EDIT: After going through the question once again, the propertyToGroupBy would have to normalise the value, i.e remove time components of the date, as the grouping is done by unique values (i.e comparison is based on isEqual:)
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.