Is there any method like tableViewWillEndDisplayingCell in iOS for UITableViewDelegate? - ios

I would like to fade cell in table view in my iPhone application before it disappears. When it is about to appear I use
- (void) tableView: (UITableView *) tableView
willDisplayCell: (UITableViewCell *) cell
forRowAtIndexPath: (NSIndexPath *) indexPath
to change alpha from zero to 1 with 0.3 sec animation.
I would like to to the same but from 1 to zero when it is about to disappear. Is it any method similar to the one above or how else can I do that?
I don't want to use standard iOS UITableViewRowAnimationFade because I don't want any other transitions while reloading table.
EDIT:
I am reloading content of the table view. Let say that I have a button which I click and it reloads data in my table. That is when I want to fade out existing cells and fade in new ones.
The controller I am using is a basic UIViewController implementing UITableViewDataSource UITableViewDelegate UIScrollViewDelegate protocols.

I found a solution.
Just before calling [self.tableView reloadData] I am animating all visible cells to fade out. I get all visible cells and iterate over them by
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows])
{
// here I animate cells to fade out
}
(thanks #AlexKoren for showing me the way to iterate over visible cells).
Because I was using [self.tableView reloadData] without any additional iOS built-in animation the cells were just disappearing. So now they are fading first and then my new cells are fading in with animation defined in
- (void) tableView: (UITableView *) tableView
willDisplayCell: (UITableViewCell *) cell
forRowAtIndexPath: (NSIndexPath *) indexPath
Important: I am reloading table view after animation is finished. If I called [self.tableView reloadData] just after iterating over cells it would reload table before animation was displayed. Because I know how long it takes to finish animation I use
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
[self.tableView reloadData];
});
with delay calculated before.

This is a bit of a hack, but should work:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//loop through visible indexPaths
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
//get each cell
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
//get its location
CGRect rectInTableView = [self.tableView rectForRowAtIndexPath:indexPath];
//set its alpha based on whether or not its going off the screen at the top.
cell.alpha = 1 + rectInTableView.origin.y / rectInTableView.size.height;
}
}

Have you tried this one?
- (void)tableView:(UITableView *)tableView
didEndDisplayingCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
I think this is what you want.

Related

didDeselectRowAtIndexPath issue

I have a tableview in a scrollview in a popover. When the view is presented, the bottom cell in tableview is not visible to the user. If I select all of the cells then deselect the fist cell, the out of view cell is deselected too. Has anyone come across this behaviour before? If so, how to approach it?
Now your job is to find all the visible cells in the tableview and then apply select/deselect to it.
UITableView *tableView = self.tableView;
// Or however you get your table view
NSArray *paths = [tableView indexPathsForVisibleRows];
// For getting the cells themselves
NSMutableSet *visibleCells = [[NSMutableSet alloc] init];
for (NSIndexPath *path in paths)
{
[visibleCells addObject:[tableView cellForRowAtIndexPath:path]];
}
// Now visibleCells contains all of the cells you care about.
-(void) tableView:(UITableView *)tableView didDeselectRowAtIndexPath:
(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath {
//stuff
//as last line:
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
For that matter, deselectRowAtIndexPath can be called from anywhere at any time you want the row to be deselected.
[self.myTableView deselectRowAtIndexPath:[self.myTableView
indexPathForSelectedRow] animated: YES];
If you are using dequeueReusableCellWithIdentifier: change your cellForRowAtIndexPath: to use dequeueReusableCellWithIdentifier:forIndexPath:
In a UITableView cells get reused. That means it only produces as many as absolutely needed. As soon as a new one is coming onto the screen, the last one is "recycled" instead of initialising a whole new instance.
This makes your application run faster. It also means that you have to undo any changes you made, when recycling.
Selection status is one of them. The UITableView should manage this automatically for you, if it is dequeued with the relevant indexPath. If not, it wouldn't know whether that specific cell should be selected.

In Objective C UITableVIew how to retain scroll position after calling reload

In Objective C UITableVIew how to retain scroll position after calling reload
While selecting a cell I call reload function of the UITableView but after reload I want the scroll to maintain its position (i.e, Position where the cell was tapped)
You can use reloadRowsAtIndexPaths:withRowAnimation: instead of reloadData.
You could do something like this.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Also if you want to scroll to some particular position, you can use this.
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
Two lines and you get the desired result:
NSArray *indexPaths = tableView.indexPathsForVisibleRows;
[tableView reloadRowsAtIndexPaths:indexPathswithRowAnimation:UITableViewRowAnimationNone];'

Validity of the UITableView Cells in ViewWill Appear

What I have
1). Container has UITableView, which has two custom UITableViewCells.
2). Core Data has certain entity which has a text to be displayed at
UITableViewCell each time I get into the View.
What i am doing ?
1) I have chosen -viewWillAppear method which gets invoked each time the view is visible.
2) In -viewWillAppear, I retrieved the data from core data.
3) Retrieved particular cell from UITableView
NSUInteger idxArr[] ={2,0}; // 2 nd section, 0th Row.
NSIndexPath *cPath = [NSIndexPath indexPathWithIndexes:idxArr length:2];
myCell *tCell = (myCell *)[self.settings cellForRowAtIndexPath:cPath];
tCell.myLabel.text = rec.servername; // rec.servername is from DC.
When I checked in the lldb,
tCell was nil.
Questions:
1) It is the right way of getting the Cell ?
2) Or, By the time -viewWillAppear, does the UITableView not Ready ?
I am sure.
You should populate the cells by conforming to tableView dataSource protocol and then in your viewWillAppear you should call reloadData on your tableView.
After calling reloadData for tableview, We need to call -scrollToRowAtIndexPath: before getting cell from -cellForRowAtIndexPath:.
Because, As we are calling a row in section 2, it might not be in the visible area until we scroll. So, cellForRowAtIndexPath: returns nil.
Method -cellForRowAtIndexPath: shouldn't be called programically. It's a data source method for UITableView and it contain some cell reuse optimalizations. If you update the view after scrolling down and up -tableView:cellForRowAtIndexPath will be called again and your changes won't be visible.
If you want to update specific cell you should update make changes in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YourCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CellId" forIndexPath:indexPath];
YourData *data = //Get your data here
if (data.isReady) {
cell.tf.text = data[indexPath.row].text;
} else {
cell.tf.text = #"Not ready yet. Need to reload this cell later";
}
return cell;
}
And then call method below when you finish fetch your data.
[self.tableView reloadRowsAtIndexPaths:(NSArray *) withRowAnimation:UITableViewRowAnimationFade];
If you want to reload whole tableView (usually it's not slow) as #salaman140 says you can call [self.tableView reloadData] to update all visible cells.
If I were you I wouldn't use:
NSUInteger idxArr[] ={2,0}; // 2 nd section, 0th Row.
NSIndexPath *cPath = [NSIndexPath indexPathWithIndexes:idxArr length:2];
I would (is much more clear):
NSIndexPath *cPath = [NSIndexPath indexPathForRow:0 inSection:2];

Animation when inserting a larger row into a UITableView assuming the wrong height

I'm having a problem in animating the addition or removal of a row in a UITableView which has a different height than other rows.
The following gifs demonstrats the issue with rows of the default height (44pts) and an larger row (100pts) being inserted and removed. The one on the left is a screen recording from the simulator (the new cell ending up covering row five is a different issue) and the one on the right is a mockup of what it should do.
In my case, I have a bunch of rows, each 60pts in height. When a button in the cell is tapped, an "edit" cell will slide out from underneath, pushing lower cells down. This edit cell is 180pts high. When I call insertRowsAtIndexPaths:withRowAnimation: or deleteRowsAtIndexPaths:withRowAnimation:, the animation assumes the wrong height of 60pts, instead of the 180pts it should be
This means that in the case of UITableViewRowAnimationTop the new cell appears at -60pts from the position it will end up at, and slides down to its new position; about a third of the animation it should be doing. Meanwhile, the row below animates smoothly from its starting position to 180pts downward, exactly as it should.
Has anyone worked out an actual solution to this? some way to tell the new row what hight it's supposed to be for the animation?
Below is the code I am using to hide and show the edit row. I'm using a TLSwipeForOptionsCell to trigger the edit, but it's easily replicated using for example tableView:didSelectRowAtIndexPath:
-(void)hideEditFields{
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
editFormVisibleForRow = -1;
[self.tableView endUpdates];
}
-(void)cellDidSelectMore:(TLSwipeForOptionsCell *)cell{
NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
// do nothing if this is the currently selected row
if(editFormVisibleForRow != indexPath.row){
if(editFormVisibleForRow >= 0){
[self hideEditFields];
// update the index path, as the cell positions (may) have changed
indexPath = [self.tableView indexPathForCell:cell];
}
[self.tableView beginUpdates];
editFormVisibleForRow = indexPath.row;
[self.tableView insertRowsAtIndexPaths:#[
[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]
] withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
}
}
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return _dataSource.count + (editFormVisibleForRow >= 0 ? 1 : 0);
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
int row = indexPath.row;
if(editFormVisibleForRow >= 0 && row > editFormVisibleForRow && row <= editFormVisibleForRow + 1){
return 180.0f;
}
else return 60.0;
}
Poking around a bit, it seems like this is a common issue with no clear answer. Most of the similar questions I've found here on SO are unanswered or offer workarounds specific to the asker's situation. (examples: Problem with RowAnimation, Custom UITableViewCell height yields improper animation, UITableView animation glitch when deleting and inserting cells with varying heights).
Also, instead of trying to make one triple-sized edit row, I tried making three smaller rows and animating them, but this was not suitable because they all appeared at once. I also tried animating them one after the other but the easing made it look odd, with an obvious 3-step animation occurring, instead of the whole edit view sliding out of view in one motion.
Edit: I've just noticed that if I call reloadRowsAtIndexPaths:withRowAnimation: UITableViewRowAnimationNone for the row above the one I'm trying to animate, it changes the behaviour of the animation; namely the animation assumes the height is 0pts, as demonstrated in the following animation. It's closer to what I want, but still not right, as the animation speed is wrong and it leaves a gap (in my app this means the background
colour pokes through)
The solution is pretty straight forward. You need to insert the cell with a height of 0, then change the height to the expected size and then call beginUpdates and endUpdates.
Here is some pseudo code.
var cellHeight: CGFloat = 0
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let dynamicHeightIndex = 2
if indexPath.row == dynamicHeightIndex {
return cellHeight
} else {
return tableView.rowHeight
}
}
func insertCell() {
// First update the data source before inserting the row
tableView.insertRows(at: [someIndexPath], with: .none)
cellHeight = 200
tableView.beginUpdates()
tableView.endUpdates()
}
To remove the cell, you'll need to wait until the updates animation completes before removing from the table view.
In iOS 11 you have the func performBatchUpdates(_:completion:) which provides a completion block. For previous versions you can try using the CATransaction completion.
cellHeight = 0
CATransaction.begin()
CATransaction.setCompletionBlock({
self.tableView.deleteRows(at: [someIndexPath], with: .none)
})
tableView.beginUpdates()
tableView.endUpdates()
CATransaction.commit()
This, using didSelectRowAtIndexPath worked for me:
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSIndexPath *pathToEditCell;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine"];
[self.tableView reloadData];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return ([indexPath isEqual:self.pathToEditCell])? 100: 44;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return (self.pathToEditCell == nil)? self.theData.count: self.theData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath isEqual:self.pathToEditCell]) {
RDEditCell *cell = [tableView dequeueReusableCellWithIdentifier:#"EditCell" forIndexPath:indexPath];
return cell;
}else{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.theData[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.pathToEditCell == nil) { // first time selecting a row
self.pathToEditCell = [NSIndexPath indexPathForRow:indexPath.row +1 inSection:indexPath.section];
[self.tableView insertRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationAutomatic];
}else if ([self.pathToEditCell isEqual:indexPath]){ // deletes the edit cell if you click on it
self.pathToEditCell = nil;
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}else{ // close the old edit cell and adds another if you click on another cell while the edit cell is on screen
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationFade];
self.pathToEditCell = indexPath;
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
For the deletions, I like the looks of the "fade" option for the animation, but "top" also was ok.

Custom UITableViewCell height yields improper animation

I currently am working with a uitableview that holds mostly standard size cells at 44pts. However, there are a couple that are larger, about 160pts.
In this instance, there are 2 rows at 44pts height, with the larger 160pts row being inserted below, at index 2 in the section.
Removal call:
- (void)removeRowInSection:(TableViewSection *)section atIndex:(NSUInteger)index {
NSUInteger sectionIndex = [self.sections indexOfObject:section];
NSIndexPath *removalPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
Delegate call:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewSection *section = [self sectionAtIndex:indexPath.section];
return [section heightForRowAtIndex:indexPath.row];
}
section call:
- (NSInteger)heightForRowAtIndex:(NSInteger)index {
StandardCell *cell = (StandardCell *)[self.list objectAtIndex:index];
return cell.height;
}
cell call:
- (CGFloat)height {
return 160;
}
What has me confused is when I remove the larger rows from the table, they start to animate, moving underneath the row above. But when they get to a certain point, about a 1/4 of the way through the animation, they disappear instead of finishing the animation.
It seems like the table animates the row with the notion that it's only 44pts, then once it's reached the point where 44pts are underneath the row above, it gets removed from the table. What detail have I overlooked that will give the table the correct notion to automatically animate the row removal?
Thanks for your help.
Update:
I tried commenting out the height function above (which overrides the default that returns 44). This results in a proper animation with no skips. FWIW
One way to solve this is to animate the row height down to 44 just before deleting:
//mark index paths being deleted and trigger `contentSize` update
self.indexPathsBeingDeleted = [NSMutableArray arrayWithArray:#[indexPath]];
[tableView beginUpdates];
[tableView endUpdates];
//delete row
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
And then in your heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.indexPathsBeingDeleted containsObject:indexPath]) {
//return normal height if cell is being deleted
[self.indexPathsBeingDeleted removeObject:indexPath];
return 44;
}
if (<test for tall row>) {
return 160;
}
return 44;
}
There's a little bit of bookkeeping going on to keep track of index paths being deleted. There are probably cleaner ways to do this. This is just the first thing that came to mind. Here's a working sample project.

Resources