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];
Related
I have a very weird question, but if I can explain it well, I guess the answer will not be so difficult.
This is my current scenario:
I have a tableView with six fixed sections.
The sections are populated with core data objects.
I am using a transient attribute to decide on which section the object must be shown. This procedure is done in a NSManagedObject subclass. The transient attribute is called sectionIdentifier.
Due to the reason of using a transient attribute to decide on which section should appear every object, I decided to create a special row on each section, with the intention that there will be always at least one row on every section. This option will reduce the problem to have a nil section that would throw exceptions in case of adding or deleting rows, at least this has been a problem sometimes while testing the app.
That means that at the first launch every section has one row (I will call it the special row). And then the user can add/delete rows perfectly.
The problem I have now is that I need this special row to be remove if the section has new rows. At the same time, if the user deletes the last row in the section, the app automatically creates a new special row, so the section is never empty.
I know that this is not the best way to manage it, but due to my iOS knowledge, this is the best way I found to avoid exceptions while adding or removing rows from an empty section.
What I need is to delete the special row from a section when the user has added at least one new row to the section.
The object has a string attribute called quitar and for the special rows: quitar = #"si". This way, all special rows are easy to identify.
I am using this code to delete the special row if there are new rows in the section:
NSManagedObjectContext *context = self.managedObjectContext;
NSEntityDescription *entity=[NSEntityDescription entityForName:#"ToDoItem" inManagedObjectContext:context];
NSFetchRequest *fetch=[[NSFetchRequest alloc] init];
[fetch setEntity:entity];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"quitar like 'si'"];
[fetch setPredicate:predicate];
NSError *fetchError;
NSArray *fetchedData=[self.managedObjectContext executeFetchRequest:fetch error:&fetchError];
for (NSManagedObject *item in fetchedData){
[context deleteObject:item];
}
I was told that you cannot use transient attributes in a NSPredicate, and you can see in my code, that all special rows are deleted, not only the special from the desired section.
I need your help to find a way to delete only the special row from the section I want, not from all the sections.
Thank you and sorry for my english.
EDITED
Please find below the way I assign sectionIdentifier in the NSManagedObject class ToDoItem:
-(NSString *)sectionIdentifier{
[self willAccessValueForKey:#"sectionIdentifier"];
NSString *tmp = [self primitiveValueForKey:#"sectionIdentifier"];
[self didAccessValueForKey:#"sectionIdentifier"];
if (!tmp){
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger comps = (NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit);
NSDate *today = [NSDate date];
NSDate *date = self.todoDueDate;
NSDateComponents *components = [[NSDateComponents alloc] init];
[components setCalendar:calendar];
[components setYear:2065];
[components setMonth:11];
[components setDay:12];
NSDate *dateFuturoSomeday = [calendar dateFromComponents:components];
NSCalendar *calendar1 = [NSCalendar currentCalendar];
NSDateComponents *components1 = [[NSDateComponents alloc] init];
[components1 setCalendar:calendar1];
[components1 setYear:2065];
[components1 setMonth:11];
[components1 setDay:13];
NSDate *dateFuturoCompleted = [calendar1 dateFromComponents:components1];
NSDateComponents *date1Components = [calendar components:comps
fromDate: today];
NSDateComponents *date2Components = [calendar components:comps
fromDate: date];
NSDateComponents *date3Components = [calendar components:comps
fromDate: dateFuturoSomeday];
NSDateComponents *date4Components = [calendar components:comps
fromDate: dateFuturoCompleted];
today = [calendar dateFromComponents:date1Components];
date = [calendar dateFromComponents:date2Components];
dateFuturoSomeday = [calendar dateFromComponents:date3Components];
dateFuturoCompleted = [calendar dateFromComponents:date4Components];
NSInteger daysAfterToday = [calendar components:NSDayCalendarUnit
fromDate:today toDate:date options:0].day;
// NSString *section;
if (daysAfterToday < 0) {
tmp = #"0";
} else if (daysAfterToday == 0) {
tmp = #"1";
} else if (daysAfterToday > 0 && daysAfterToday < 2) {
tmp = #"2";
}
else if ([self.todoDueDate isEqualToDate:dateFuturoSomeday]){
tmp = #"4";
}
else if ([self.todoDueDate isEqualToDate:dateFuturoCompleted]){
tmp = #"5";
}
else {
tmp = #"3";
}
[self setPrimitiveValue:tmp forKey:#"sectionIdentifier"];
}
return tmp;
}
I have checked that at the cellForRowAtIndexPath method, I am able to detect the value from the transient attribute sectionIdentifier. That means that I can use it to identify the special row item, update the value of another attribute an delete it.
ToDoItem *todoitem = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString *sec = todoitem.sectionIdentifier;
NSString *quitar = todoitem.quitar;
if ([sec isEqualToString:#"1"] && [quitar isEqualToString:#"si" ] && ( numObjectsSec1 > 1 ) ){
NSLog(#"DAS OBJEKT KANN GELĂ–SCHT WERDEN %#", todoitem.todoName);
NSManagedObjectContext *context = self.managedObjectContext;
[todoitem setValue:#"si se puede borrar" forKey:#"borrar" ];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
[context deleteObject: todoitem];
};
I have two arrays one of which is a tweets array and contains twitter tweets. Another of which is a instapics array and contains Instagram pictures. Both of these Arrays have different keys for date. The twitter one had created_at and the instagram one has created_time. I want to display both of them on a single UITableView and have them organized by date. Here is what I am doing so far, the problem with this however is that it only shows Instagram Pictures:
- (void)sortArrayBasedOndate {
NSDateFormatter *fmtDate = [[NSDateFormatter alloc] init];
[fmtDate setDateFormat:#"yyyy-MM-dd"];
NSDateFormatter *fmtTime = [[NSDateFormatter alloc] init];
[fmtTime setDateFormat:#"HH:mm"];
totalFeed = [totalFeed sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
// Instagram Date Retrieval
NSDictionary *instagram = self.instaPics;
NSString *createdAt = instagram[#"created_time"];
int createdAtN = [createdAt intValue];
NSTimeInterval timestamp = (NSTimeInterval)createdAtN;
NSDate *date1 = [NSDate dateWithTimeIntervalSince1970:timestamp];
// Twitter Date Retrieval
NSDictionary *twitter = self.tweets;
NSString *twitterCreated = twitter[#"created_at"];
int createdAtTwitter = [twitterCreated intValue];
NSTimeInterval timestampTwitter = (NSTimeInterval)createdAtTwitter;
NSDate *date2 = [NSDate dateWithTimeIntervalSince1970:timestampTwitter];
return [date1 compare:date2];
}];
}
The above is how I am trying to organize them on the array, below is how I am attempting to display them:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [totalFeed objectAtIndex:indexPath.row];
if ([tweets containsObject:object]) {
static NSString *Twitter = #"TweetCell";
UITableViewCell *twitter = [self.tableView dequeueReusableCellWithIdentifier:Twitter];
NSDictionary *totalArray = totalFeed[indexPath.row/2];;
// Creation Code Not shown
return twitter;
}else{
static NSString *Instagram = #"InstagramCell";
UITableViewCell *instagram = [self.tableView dequeueReusableCellWithIdentifier:Instagram];
NSDictionary *entry = instaPics[indexPath.row / 2];
// Creation Code not Shown
return instagram;
}
}
The easiest solution would be if you add a common "creationDate" key to all objects in the
totalFeed array. The value of this key should be an NSDate created from
the "created_at" or "created_time" key.
Then you can just sort the array according to this key:
NSSortDescriptor *sortDesc = [[NSSortDescriptor alloc] initWithKey:#"creationDate" ascending:YES]];
totalFeed = [totalFeed sortedArrayUsingDescriptors:#[sortDesc]];
If you cannot do that for some reason, you have to fix the comparator method, it does
not use the passed arguments obj1, obj2 at all.
Something like (pseudo-code):
totalFeed = [totalFeed sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSDate *date1, *date2;
if ("obj1 is a Twitter") {
date1 = "get created_at from twitter obj1"
} else {
date1 = "get created_time from instagram obj1"
}
if ("obj2 is a Twitter") {
date2 = "get created_at from twitter obj2"
} else {
date2 = "get created_time from instagram obj2"
}
return [date1 compare:date2];
}];
In any case, in cellForRowAtIndexPath you have to access totalFeed[indexPath.row]
(without dividing by 2, which does not make sense here).
More sample code:
NSArray *instaPics; // your instagrams
NSArray *tweets; // your tweets
NSMutableArray *totalFeed = [NSMutableArray array]; // the common array
// Date formatter for the tweets. The date format must exactly
// match the format used in the tweets.
NSDateFormatter *fmtDate = [[NSDateFormatter alloc] init];
[fmtDate setDateFormat:#"..."];
// Add all instagrams:
for (NSMutableDictionary *instagram in instaPics) {
NSString *createdAt = instagram[#"created_time"];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:[createdAt doubleValue]];
instagram[#"creationDate"] = date;
[totalFeed addObject:instagram];
}
// Add all tweets:
for (NSMutableDictionary *twitter in tweets) {
NSString *twitterCreated = twitter[#"created_at"];
NSDate *date = [fmtDate dateFromString:twitterCreated];
twitter[#"creationDate"] = date;
[totalFeed addObject:twitter];
}
// Sort
NSSortDescriptor *sortDesc = [[NSSortDescriptor alloc] initWithKey:#"creationDate" ascending:YES];
[totalFeed sortUsingDescriptors:#[sortDesc]];
I wrote this code to reload the UItableView with an events that has the same date as the current date when the user click on todays events UIButton in the main view controller but the problem is the below code is not reloading the right data (it just gives the initial data without comparing the date of the event with the date of the calendar in the IPhone), my data comes from a json file within the project and consists from NSArray of events, each has a different value for each key and one of these keys is the data of that event ("date"), can anyone plz clarify for me why the below code is not returning the right data ??
#implementation MainViewController {
NSArray *_events;
}
....
- (IBAction)upcomingEvents:(id)sender {
NSDate *currDate = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:#"dd-MM-YYYY"];
NSString *dateString = [dateFormatter stringFromDate:currDate];
for (Events *event in _events){
if([event.date isEqualToString:dateString]){
[self.myTableView reloadData];
}
}
}
If you're using a UITableViewDataSource you should make sure that it returns only the events that match your condition [event.date isEqualToString:dateString]
You can do
NSArray * dateEvents = [_events filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Events * event, NSDictionary *bindings)
{
return [event.date isEqualToString:dateString];
}];
Then you can use dateEvents for your UITableViewDataSource.
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.
I'm writing an app that displays some data which changes based upon the day of the week it is. The context of the app is that it is for a conference that is upcoming. I want to display the calendar entries for that day. The event detail being stored in coredata.
What I'm using to create the NSPredicate is:
NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setDateFormat:#"yyyy-MM-dd"];
NSDate *today = [NSDate date];
NSString *sunday = #"2011-12-04";
NSString *monday = #"2011-12-05";
NSString *tuesday = #"2011-12-06";
NSString *wednesday = #"2011-12-07";
NSInteger dayNumber = 1;
if ([[df stringFromDate:today] isEqualToString:sunday]) {
dayNumber = 2;
} else if ([[df stringFromDate:today] isEqualToString:monday]) {
dayNumber = 3;
} else if ([[df stringFromDate:today] isEqualToString:tuesday]) {
dayNumber = 4;
} else if ([[df stringFromDate:today] isEqualToString:wednesday]) {
dayNumber = 5;
}
NSExpression *lhs = [NSExpression expressionForKeyPath:#"day_number"];
NSExpression *rhs = [NSExpression expressionForConstantValue:[NSNumber numberWithInt:dayNumber]];
NSPredicate *equalToPredicat = [NSComparisonPredicate predicateWithLeftExpression:lhs
rightExpression:rhs
modifier:NSDirectPredicateModifier
type:NSEqualToPredicateOperatorType
options:0];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptorTime = [[NSSortDescriptor alloc] initWithKey:#"start_time" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:/*sortDescriptorDayNumber,*/ sortDescriptorTime, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:equalToPredicat];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"day_number"
cacheName:#"TodayCache"]
What I am seeing though is that when the date changes (ie: Saturday to Sunday) the data (displayed in a UITableView) doesn't get updated.
Thanks,
Matt.
The comment above is correct. Some more info.
1) The table will only update when [tableView reloadData] or one of the other reload methods is called
2) If you set a delegate for the NSFetchedResultsController you can respond to model changes and reload the table
3) The date changing is not a model change (unless you are updating the date in the model itself) and the NSFetchedResultsController will only update itself if it detects that it's managed object context has changed.
4) You need to do fetchedResultsController:performFetch:error when when the day changes b/c you are changing the predicate (i.e. the request). Otherwise, the table will reload with the old data.
To refresh the table based on the date changing, you need to detect the date change at whatever interval you like and reload the table at that time. One way you could do it is to calculate the interval between the current time and midnight and set a timer to fire after that interval. When the timer fires, change the predicate, performFetch with the results controller and reload the table. Then set a new timer until the next midnight. If you want this to be done in the users time zone, you'll need to take that into account also.
EDIT: clarified answer that a performFetch is necessary when the predicate changes