Anchor a row with NSFetchedResultsController - ios

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...

Related

How to prevent overflow of adjustable height tableview cell on delete

In a UITableView Controller, I have just added 'swipe to delete' by implementing tableView: commitEditingStyle: forRowAtIndexPath. Additionally, the rows can be selected to expand showing more content.
The undesired result after swiping:
The two lower rows remain in view after swiping until about about 0.5 seconds after the undelete animation completes.
A screenshot of IB:
The cell's contents have grown into the lower cell without it showing that it has been selected. (Selection causes the cell to increase height and give it a grayish background color.) This is occurring on every row in 2 similarly operating view controllers.
I have tried (without success) to intercept the 'selection' in several UITableViewDelegate methods, and cannot find out how to stop this from occurring. I have also tried setting the IB dynamic prototype cells to height: 85.
Looking for ideas on how to prevent this expansion from occurring.
EDIT
- (void)viewDidLoad {
....
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = kCellHeight;
....
}
#pragma mark - TableView delegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
NSInteger *rows = (NSInteger *)[sectionInfo numberOfObjects];
if (!self.rowsInSection)
self.rowsInSection = rows;
if (rows > 0)
return [sectionInfo numberOfObjects];
else {
[tableView setSeparatorColor:[UIColor clearColor]];
[tableView setBounces:NO];
return 1;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = self.rowsInSection > 0 ? #"numberIdentifier" : #"noNumbersIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
if (self.rowsInSection > 0)
[self configureCell:cell atIndexPath:indexPath];
else
cell.selectionStyle = UITableViewCellSelectionStyleNone;
// [self.arrayOfIndexPaths addObject:indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if (selectedIndexPath) {
if (tableView.editing)
return 85.0;
else if (selectedIndexPath.row == indexPath.row)
return 185.0;
}
return 85.0;
}
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)path {
if (tableView.editing)
return nil;
// If real rows exist, return the path, making row selectable
if (self.rowsInSection > 0)
return path;
// Otherwise do not allow the row to be selected
return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][0];
if ([sectionInfo numberOfObjects] > 0)
// Return the contentView to stop the header from sliding with delete
return [tableView dequeueReusableCellWithIdentifier:#"numberHeaderIdentifier"].contentView;
else
return [tableView dequeueReusableCellWithIdentifier:#"emptyHeaderIdentifier"];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 75;
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Number *aNumber = [self.fetchedResultsController objectAtIndexPath:indexPath];
[cell configureSubviewsInCell:cell withNumber:aNumber];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView beginUpdates];
[tableView endUpdates];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
}
You should set the hidden property of the labels that you don't want to show when the table view cell is not selected. For example:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Number *aNumber = [self.fetchedResultsController objectAtIndexPath:indexPath];
UILabel *label1 = (UILabel *)[cell.contentView viewWithTag:501];
label1.text = [aNumber valueForKey:#"number"];
if (!cell.selected)
{
label1.hidden = YES;
}
else
{
label1.hidden = NO;
}
.....
}
Then in didSelectRowAtIndexPath:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView beginUpdates];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UILabel *label1 = (UILabel *)[cell.contentView viewWithTag:501];
label1.text = [aNumber valueForKey:#"number"];
label1.hidden = NO;
[tableView endUpdates];
}
You should look into subclassing UITableViewCell so you don't have to use tags to access subviews.
The 'hiding' solution posted by beowulf is a valid option. In addition and because the swipe (to begin editing) was causing subviews in the cell to become 'unclipped', a method needed to be overrided, like so:
// this method is called as the swipe to delete is started
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
// only hide for unexpanded (unselected) cells
if ([self.selectedRowIndex compare:indexPath] != NSOrderedSame)
{
NumberTableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// a cell subclass method to hide/unhide subviews that fall into
// the next cell below
[cell subViewsInCellShouldBeHidden:YES];
}
}

UITableviewCell height not getting reset on scroll

I have a tableview which can be expanded on selecting the cell and collapses on selecting again. When you select, the cell should expand and display a label and when you select again it collapses and hides the label . The expanding and collapsing works fine, but if i scroll the tableview after expanding a cell it behaves weird. Once it goes out of the view and comes back , the cell will have the expanded cell's height but the label which is supposed to be shown in expanded cell is hidden.If i select the cell again it collapses and displays the label. I use ,
- (CGFloat)tableView:(UITableView *)t heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self tableView:t cellForRowAtIndexPath:indexPath];
if([self cellIsSelected:indexPath])
return cell.frame.size.height+35;
return cell.frame.size.height;
}
- (BOOL)cellIsSelected:(NSIndexPath *)indexPath {
// Return whether the cell at the specified index path is selected or not
NSNumber *selectedIndex = [self.selectedIndexes objectForKey:indexPath];
return selectedIndex == nil ? FALSE : [selectedIndex boolValue];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Deselect cell
NSLog(#"Select cell:%#",indexPath);
[self.tableView deselectRowAtIndexPath:indexPath animated:TRUE];
if([self pickTaskForIndexPath:indexPath].productSpecialMessage){
BOOL isSelected = ![self cellIsSelected:indexPath];
NSNumber *selectedIndex = [NSNumber numberWithBool:isSelected];
[self.selectedIndexes setObject:selectedIndex forKey:indexPath];
PickTaskTableviewCell *cell= [self.tableView cellForRowAtIndexPath:indexPath];
cell.message.hidden=false;
cell.messageLabel.text=[self pickTaskForIndexPath:indexPath].productSpecialMessage;
cell.messageLabel.lineBreakMode=NSLineBreakByTruncatingTail;
cell.messageLabel.numberOfLines=3;
if(cell.messageLabel.hidden==true){
cell.messageLabel.hidden = false;
} else {
cell.messageLabel.hidden = true;
}
NSLog(#"message:%#",cell.messageLabel.text);
[cell layoutIfNeeded];
}
self.tableView.rowHeight=UITableViewAutomaticDimension;
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
indexPath is added to the selectedIndexes on didSelectRowAtIndexPath
Please help me
Cells should be configured only within cellForRowAtIndexPath. When a state change occurs that makes a cell need to look different, just reload that cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PickTaskTableviewCell *cell = (PickTaskTableviewCell *)[tableView dequeueReusableCellWithIdentifier:#"cell"];
// everything else you do to configure the cell goes here, then ...
// check the logic here, we want one condition that tells us whether to show the labels
if([[self cellIsSelected:indexPath] && self pickTaskForIndexPath:indexPath].productSpecialMessage){
// don't need these here
//NSNumber *selectedIndex = [NSNumber numberWithBool:isSelected];
// [self.selectedIndexes setObject:selectedIndex forKey:indexPath];
// PickTaskTableviewCell *cell= [self.tableView cellForRowAtIndexPath:indexPath];
cell.message.hidden=false;
cell.messageLabel.text=[self pickTaskForIndexPath:indexPath].productSpecialMessage;
cell.messageLabel.lineBreakMode=NSLineBreakByTruncatingTail;
cell.messageLabel.numberOfLines=3;
cell.messageLabel.hidden=NO;
} else {
cell.message.hidden=YES;
cell.messageLabel.hidden=YES;
}
NSLog(#"message:%#",cell.messageLabel.text);
// don't need this here
// [cell layoutIfNeeded];
return cell;
}
Selection (and presumably deselection) cause the need to update the cell, so...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// don't deselect it here, just reload it
// more on this later...
[self.selectedIndexes setObject:selectedIndex forKey:indexPath];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
// probably do the same in didDeselectRowAtIndexPath:
One last (optional) point. There's no need to maintain your own list of selected index paths, UITableView does that for you, so you could delete your selectedIndexes property and just use the table view methods, e.g....
- (BOOL)cellIsSelected:(NSIndexPath *)indexPath {
// Return whether the cell at the specified index path is selected or not
return [[self.tableView indexPathsForSelectedRows] containsObject:indexPath];
}

One ViewController with 2 TableViews - App crashes

I have a one View controller managing 2 tableviews. I use a flag to track which table is selected. In each of the delegate functions I just check the flag and use the right table.
Everything works great except that when i load the second table which has lesser items than the first one, crashes when I scroll the table , for the following error.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no object at index 2 in section at index 0'
* First throw call stack:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"Drawing Row = %d Total num Of items = %d", indexPath.row, [[self.fetchedResultsControllerComments fetchedObjects] count]);
Prints this:
Drawing Row = 2 Total num Of items = 0
If the number of items in this table is correct, then why is this function getting called in the first place?
Here is the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(currentSelectionTableType1)
{
// Draw first kind of cell.
PlainImageCell *cell1 = [tableView dequeueReusableCellWithIdentifier:#"ImageCell"];
if(cell1 == nil)
cell1 =[[PlainImageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"ImageCell"];
[self configureCell1:cell1 atIndexPath:indexPath];
return cell1;
}
// else Draw the second kind of cell
PlainTextCell *cell2 = [tableView dequeueReusableCellWithIdentifier:#"TextCell"];
if(cell2 == nil)
cell2 =[[PlainTextCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"TextCell"];
[self configureCell2:cell2 atIndexPath:indexPath];
return cell2;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
if(currentSelectionTableType1)
return [[self.fetchedResultsControllerDataSource1 sections] count];
return [[self.fetchedResultsControllerDataSource2 sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo;
if(currentSelectionTableType1)
{
sectionInfo = [self.fetchedResultsControllerDataSource1 sections][section];
}
else
{
sectionInfo = [self.fetchedResultsControllerDatasource2 sections][section];
}
return [sectionInfo numberOfObjects];
}
Thx
EDIT - based on the code you added:
You need to define one cell before your conditional and then configure that cell based on the conditional and then return the cell after the conditional. If you need both an ImageView and a TextCell, you can configure those objects in the conditional code.
Why not just use one TableView with two datasources and switch out the datasources as needed?
Something like this:
#property(nonatomic, strong) NSArray *tableViewDataSource1;
#property(nonatomic, strong) NSArray * tableViewDataSource2;
#property(nonatomic) BOOL usingDataSource2;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.usingDataSource2) {
return [self.tableViewDataSource2 count];
}
return [self. tableViewDataSource1 count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Create the cell before conditional
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"reuseIdentifier" forIndexPath:indexPath];
// Conditionally configure the cell
if (self.usingDataSource2) {
// Configure Cell using self.tableViewDataSource2 data
} else {
// Configure Cell using self.tableViewDataSource1 data
}
// Return the configured cell after the conditional
return cell;
}

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;
}

NSFetchedResultsController prepend a row or section

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.

Resources