NSFetchedResultsController prepend a row or section - ios

I have a UITableView populated with a standard NSFetchedResultsController. However I'd like to prepend a row or a section (row preferably but either would works fine really.)
The only way I can possibly see doing this right now is to rewrite all the NSIndexPath's manually when dealing with the section/rows dealing with the data from NSFetchedResultsController to trick it into seeing section at index 0 and starting with row at index 0. This however seems like a really bad idea that would quickly get confusing so I'd like to preferable avoid that.
A good example of this would be in the official Twitter app when you start it up for the first time and I walks you through adding some people from your friends list.
The red section is pretty much what I'd like to achieve, and the yellow section I assume is the results from an NSFetchedResultsController in the same section (though with their custom styling it might be a separate section.)

This is possible to do in a fairly clean way.
I'm assuming you're starting with a standard tableview set up with a standard NSFetchResultsController that uses Apple's sample code.
First you need two utility functions:
- (NSIndexPath *)mapIndexPathFromFetchResultsController:(NSIndexPath *)indexPath
{
if (indexPath.section == 0)
indexPath = [NSIndexPath indexPathForRow:indexPath.row+1 inSection:indexPath.section];
return indexPath;
}
- (NSIndexPath *)mapIndexPathToFetchResultsController:(NSIndexPath *)indexPath
{
if (indexPath.section == 0)
indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section];
return indexPath;
}
These should be fairly self explanatory - they're just helpers to deal with adding the extra row when we want to use an index path from the fetched results controllers to access the table, or removing it when going the other way.
Then we need to create the extra cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"MyCellId";
if (indexPath.section == 0 && indexPath.row == 0)
{
UITableViewCell *cell;
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleGray;
cell.textLabel.text = NSLocalizedString(#"Extra cell text", nil);
return cell;
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
make sure we configure it correctly (configurecell will only be called for cells from the fetch results controller):
// the indexPath parameter here is the one for the table; ie. it's offset from the fetched result controller's indexes
- (void)configureCell:(SyncListViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
indexPath = [self mapIndexPathToFetchResultsController:indexPath];
id *obj = [fetchedResultsController objectAtIndexPath:indexPath];
<... perform normal cell setup ...>
}
and tell the tableview it exists:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger numberOfRows = 0;
if ([[fetchedResultsController sections] count] > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
numberOfRows = [sectionInfo numberOfObjects];
}
if (section == 0)
numberOfRows++;
return numberOfRows;
}
and respond to selection:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (indexPath.section == 0 && indexPath.row == 0)
{
[self doExtraAction];
return;
}
... deal with selection for other cells ...
and then remap any updates we get from the results controller:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
indexPath = [self mapIndexPathFromFetchResultsController:indexPath];
newIndexPath = [self mapIndexPathFromFetchResultsController:newIndexPath];
switch(type) {
... handle as normal ...

I understand your concerns about complexity, but it is really just adding 1 to numberOfRowsInSection: and adding 1 to indexPath.row in cellForRowAtIndexPath: (beside adding the code for row 0).
Another solution would not have to be very elaborate to become even more cumbersome.
That being said, it really seems that the "heading" you are proposing is a typical candidate for a section header.

Related

Why UITableView is not dequeuing reusable cells?

I'm adding subviews with UITableViewCell default type. In this, assume that, I've only 2 rows. At first, when it comes to tableView:cellForRowAtIndexPath: I'll generate a dynamic identifier for each cell based on indexPath.row. So it goes well and create a new cell inside if(cell == nil) condition. I'm adding subviews inside this condition only.
Next, out side that condition, I'm simply updating content of subview by fetching them out of the cell's contentView. This works great.
Problem is here : I'm expanding my cell (on which user tapped). When I do this, it'll go into tableView:cellForRowAtIndexPath: and I'm able to see the same identifier generated before. However, its going inside if(cell == nil) condition.
For a note, I'm checking for that condition like this,
if(!cell) and not if(cell == nil).
Can you guess what is the issue? However in my deep investigation, I found that, it'll only goes inside if(cell == nil) once I expand particular row and not next time for the same row.
Here's the sample :
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//number of count
return [array count];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//dynamic height will be provided based on content.
return [self heightForRow:indexPath];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier = [NSString stringWithFormat:#"cellIdentifier_%ld", (long)indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if(!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
[self setupCell:cell forRow:[indexPath row] inTable:tableView];
}
[self updateCell:cell forRow:[indexPath row] inTable:tableView];
return cell;
}
- (void) setupCell:(UITableViewCell *)cell forRow:(NSInteger)row inTable:(UITableView *)tableView {
//setting up view here
}
- (void) updateCell:(UITableViewCell *)cell forRow:(NSInteger)row inTable:(UITableView *)tableView {
//updating view here
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.expandedIndexPath = ([indexPath compare:self.expandedIndexPath] == NSOrderedSame) ? nil : indexPath;
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
[tableView scrollToRowAtIndexPath:self.expandedIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
Hope my issue is clear to you. Please comment if you don't get something or found anything missing here (and I'll update it here) or answer if you know what's the solution to this.
Thanks!

Get Checkmarked Rows and Send to Console

All I'm looking to do is get the selected (checkmarked) rows from my UITableView and show them in my console log. Doesn't seem like it should be so difficult. I've found two methods that I'll display below. Neither work despite the logic mostly making sense to me. Which would you suggest and how can I tweak to make it work?
My TableView Code:
I don't think this is completely necessary to the issue, but I know it sometimes helps to see the whole picture.
#pragma mark - tableView datasource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [self.places count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSDictionary *tempDictionary= [self.places objectAtIndex:indexPath.row];
cell.textLabel.text = [tempDictionary objectForKey:#"name"];
if([tempDictionary objectForKey:#"vicinity"] != NULL)
{
cell.detailTextLabel.text = [NSString stringWithFormat:#"%#",[tempDictionary objectForKey:#"vicinity"]];
}
else
{
cell.detailTextLabel.text = [NSString stringWithFormat:#"Address Not Available"];
}
return cell;
}
//Handles tableView row selection and addition and removal of the checkmark
- (void)tableView:(UITableView *)theTableView
didSelectRowAtIndexPath:(NSIndexPath *)newIndexPath {
[theTableView deselectRowAtIndexPath:[theTableView indexPathForSelectedRow] animated:NO];
UITableViewCell *cell = [theTableView cellForRowAtIndexPath:newIndexPath];
if (cell.accessoryType == UITableViewCellAccessoryNone) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
// Reflect selection in data model
} else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
// Reflect deselection in data model
}
}
Method 1:
Add a conditional statement to the end of the checkmark handler to add/remove selections to and from an array. Then create a button action that simply calls the array and displays it in the console. I think this is clunky but could work.
//Handles tableView row selection and addition and removal of the checkmark
- (void)tableView:(UITableView *)theTableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[theTableView deselectRowAtIndexPath:[theTableView indexPathForSelectedRow] animated:NO];
UITableViewCell *cell = [theTableView cellForRowAtIndexPath:indexPath];
if (cell.accessoryType == UITableViewCellAccessoryNone) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
//Reflect selection in data model
} else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
//Reflect deselection in data model
}
if ([[theTableView cellForRowAtIndexPath:indexPath] accessoryType] == UITableViewCellAccessoryCheckmark) {
[_selectedCellIndexes addObject:indexPath];
}
}
- (IBAction)sendResults:(id)sender {
NSLog(#"Add these: %#", _selectedCellIndexes);
}
Method 2:
Get the selected rows AND send to console log only when button is tapped. This seems to be the more logical method, but I can't seem to get it to work either. It doesn't throw any errors, but returns "Selected Items: (null)" in the console. What have I missed?
//Sends checkmarked items to console log
- (IBAction)sendResultsOption1:(id)sender {
NSMutableArray *aList = [[NSMutableArray alloc] init];
for (NSIndexPath *indexPath in _tableView.indexPathsForSelectedRows) {
NSString *r = [NSString stringWithFormat:#"%li",(long)indexPath.row];
[aList addObject:r];
}
NSLog(#"Selected Items: %#", _aList);
}
For what it's worth, I've also followed the instructions here without any luck. Hope you guys can help. Thanks in advance!
In Method 1, your method looks like this:
- (void)tableView:(UITableView *)theTableView
didSelectRowAtIndexPath:(NSIndexPath *)newIndexPath {
But you're referring to indexPath in the body. You don't have an indexPath (the undeclared identifier), but you have a newIndexPath, so at a minimum, this is the start of your problems and should be fixed first.
Giving your variables the right names looks like it should work for this approach...
In Method 2, the problem is none of your table view cells are selected. In you didSelectRowAtIndexPath method, you check the accessory icon to a check mark, then you deselect the row. So there are no objects in _tableView.indexPathsForSelectedRows.
In this approach, you need to change your for loop. Instead you need to iterate through every index path, and check on the accessory icon. If it's a check mark, add it to the array. Now log the array.
As far as which approach would be preferable, it depends on how you intend to use this ultimately. Obviously, the end goal isn't to NSLog the checkmarked rows--this is an iOS app we're talking about.

Bug when I try to check only one cell in tableview

When I check the first cell after loading ā€“ nothing happens, Iā€™m tapping over and over again ā€“ nothing happens. I can check other cells, the second, the third etc. and only after that I can check the first cell. This is my method:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger row = indexPath.row;
NSUInteger oldRow = lastIndexPath.row;
if (oldRow != row) {
UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath];
newCell.accessoryType = UITableViewCellAccessoryCheckmark;
UITableViewCell *oldCell = [tableView cellForRowAtIndexPath:lastIndexPath];
oldCell.accessoryType = UITableViewCellAccessoryNone;
lastIndexPath = indexPath;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Or maybe you can advise other way to make it (checking only one cell in tableview), because I've found only models with a lot of code and hard to understand.
It is because at first your lastIndexPath variable is nil, so lastIndexPath.row will return 0. If you tap on the first row, that row is also 0, so it won't enter the if statement. Replace that statement with: if (!lastIndexPath || oldRow != row)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell;
//cell creation code
cell.accessoryType = nil != lastIndexPath && lastIndexPath.row == indexPath.row ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray* reloadRows = nil == lastIndexPath ? #[indexPath] : #[lastIndexPath, indexPath];
lastIndexPath = indexPath;
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[tableView reloadRowsAtIndexPaths:reloadRows withRowAnimation: UITableViewRowAnimationAutomatic];
}

UITableView Cells With DIfferent Format & Behavior in Same Table Section

I have a UITableView with one section. All of the cells in that one section have cells that are derived from a subclass of UITableViewCell called PLContactCell.
What I'd like to do is, for the very last row of the table only, not use a PLContactCell. I just want to use a normal UITableViewCell that I can format however I would like. I'd also like to be able to have this cell NOT respond to being tapped.
My initial cellForRowAtIndexPath method is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PLContactCell *cell = [tableView dequeueReusableCellWithIdentifier:[PLContactCell reuseIdentifier]];
if (!cell) {
cell = [PLContactCell reusableCell];
cell.delegate = self;
}
id modelObject = [[sections objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
if ([modelObject isKindOfClass:[NSString class]]) {
[cell configureWithString:modelObject];
} else {
[cell configureWithUser:modelObject];
}
return cell;
}
EDIT
So I tried created a UITableView cell in the XIB file and added the reuse identifier of "newCell" to it. Then I added this code:
if (indexPath.row == [[sections objectAtIndex:indexPath.section] count] - 1) {
NSString *CellIdentifier = #"newCell";
noFormatCell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
}
This doesn't do anything. My question is, how do I access the last row of the section and how do I make it so that that cell it is not a PLContactCell but a UITableView Cell.
If it's always at the end, you might consider using the footer view of the UITableView. You can then keep some extra logic out of your UITableViewDataSource.
If it HAS to be as a cell, you'd have to add an extra row count on your last section, then do an if statement check to watch out for it in your -tableView:cellForRowAtIndexPath: implementation. I would strongly urge you try the footer approach, as it's cleaner and way easier to figure out what you were doing a few months/years from now.
Here's some code. Note you'd need to make another section if you are using grouped style in the UITableView.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == sections.count - 1) //Looking for last section
{
return [sections objectAtIndex:section].count + 1; //Add one to the last section
}
return [sections objectAtIndex:section].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
if ((sections.count == indexPath.section) && [sections objectAtIndex:indexPath.section].count == indexPath.row)
{
//Here you would create and return your UITableViewCell
}
PLContactCell *cell = [tableView dequeueReusableCellWithIdentifier:[PLContactCell reuseIdentifier]];
if (!cell) {
cell = [PLContactCell reusableCell];
cell.delegate = self;
}
id modelObject = [[sections objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
if ([modelObject isKindOfClass:[NSString class]]) {
[cell configureWithString:modelObject];
} else {
[cell configureWithUser:modelObject];
}
return cell;
}

Anchor a row with NSFetchedResultsController

I'm trying to make it so my UITableView always has a specific row at the bottom, anchored there.
I'm using a NSFetchedResultsController to perform a fetch, and then the regular Apple boilerplate for detecting a merged context change.
What I'd like to do is always have one row at the bottom of the results, "Not What You're Looking For?". How would I do this? I'm comfortable with custom cell types, but I can't get even a cell of the same type to anchor to the bottom.
Code that adds one more row than what is in the fetchedResultsController:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return[sectionInfo numberOfObjects]+1;
}
Code for cellForRowForIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifierNormal = #"NormalCell";
UITableViewCell *cell = nil;
cell = [tableView CellIdentifierNormal];
if (cell == nil) {
cell = [[NormalCell createCell]autorelease];
}
// For regular results, go configure the cell. For the extra 'Special Row' at bottom, assign it the Special cell.
if([indexPath indexAtPosition:0] <= [[[fetchedResultsController sections] objectAtIndex:0] numberOfObjects])
[self configureCell:cell atIndexPath:indexPath];
else {
if (cell == nil) {
cell = [[SpecialCell createCell] autorelease];
}
SpecialCell *nc = (SpecialCell*)cell;
nc.labelFirstLine.text = #"Not What You're Looking For?";
}
return cell;
}
This would work if not using a NSFetchedResultsController, but what happens is that whenever a cell is updated, the method 'configureCell' is being called from the Controller, which knows nothing about SpecialCell.
From - (void)controller:(NSFetchedResultsController *)controller didChangeObject:......
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
And here is configureCell:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// Configure the cell
// Go get it from coredata
NormalCell *vc = (NormalCell*)cell;
NormalObject *no = (NormalCell *)[fetchedResultsController objectAtIndexPath:indexPath];
....<ASSIGNMENT TO CELL HERE>....
}
in cellForRowAtIndexPath when you are creating cell for different rows from array, then keep the condition that if it reaches the end of array then you add your custom cell with a string that you want...
For more information please add the code...

Resources