Don't animate UITableView cells that have already appeared - ios

I have added some animation to a table view to make it look nicer when it loads. In the controller's viewDidLoad I make an asynchronous request for data and when it returns the table view is populated.
When my table loads the cells are revealed one by one. (I took inspiration from this excellent guide).
- (void)tableFadeInAnimation {
//[_venueTableView reloadData];
NSArray<UITableViewCell *> *cells = _venueTableView.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:nil];
NSLog(#"end of table animation");
index += 1;
}
}
My problem with running this as an initialising function is that once this finishes my table has no more animations to perform. I then took this principle to cellForRowAtIndexPath (removing the loop).
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
This would load all the cells together but would animate new cells appearing on the table.
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
This made the table load each cell 1 by 1 however it is tied to all the cells (not the visible ones) so the further you go down the table, the longer the loading time for the cell.
Also when you move back up the table, all the older cells reanimate onto the table. I want the old cells to remain and the new cells to animate. Is there a way I can keep track of which cells have been loaded and only animate brand new, never before seen cells?

You should have a property to keep track index of last cell which is displayed (name lastCellDisplayedIndex). Only animate cells which have index less than lastCellDisplayedIndex. Each time call reloadData, reset lastCellDisplayedIndex = -1.
Try my below code.
#property (nonatomic, assign) NSUInteger lastCellDisplayed;
- (void)viewDidLoad {
[super viewDidLoad];
[self reloadTableView];
}
// Use this method each time you want to reload data of tableView
// instead of |reloadData| method
- (void)reloadTableView {
_lastCellDisplayedIndex = -1;
[self.tableView reloadData];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Update |_lastCellDisplayedIndex| each time a cell is displayed
_lastCellDisplayedIndex = MAX(indexPath.row, _lastCellDisplayedIndex);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// Only animate cells which have |indexPath.row| < |_lastCellDisplayedIndex|
if (_lastCellDisplayedIndex < indexPath.row) {
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
} else {
cell.alpha = 1;
}
...
}

The best approach for this is to add your animation block and any change to your cell's frame or alpha, in the tableView:willDisplayCell:forRowAtIndexPath: method of your UITableViewDelegate

My recommended approach, assuming you have some backing data source to provide data in cellForRowAtIndexPath, is to add some mutable property hasBeenDisplayed to your model objects (or an NSDictionary that maps each model object to a bool that indicates whether or not it has been displayed). This logic is complicated enough that you want some logic code to ensure the consistency of the view code. Then, once you have this property, you can call your custom animation in cellForRowAtIndexPath if the cell has not yet been displayed.

Thanks to #trungduc for the answer, I'm posting the completed solution to this in the hopes people will find it useful. To stop the table drawing cells that have already appeared you need to implement a variable to track the maximum index that has been displayed on the table, lastCellDisplayedIndex. In #trungduc's answer he put this variable in the - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method however I found this created some errors making some cells redraw themselves. I had a read up on the difference between the cellForRow and cellWillDisplay methods and it seemed like the best place to put the animation was cellWillDisplay as the cell has been initialised and is apparently the place you should be performing UI tweaks to a cell (like animations!).
This method looks like this:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
NSLog(#"lastCellDisplayedIndex = %ld, indexPath for Cell = %ld", lastCellDisplayedIndex, indexPath.row);
if (lastCellDisplayedIndex <= indexPath.row){
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
if (lastCellDisplayedIndex == totalCellsToDisplay - 1){
NSLog(#"END OF TABLE ANIMATIONS!");
lastCellDisplayedIndex = totalCellsToDisplay + 1;
}
}
else {
cell.alpha = 1;
}
}
This method handles almost everything. It will first change the value of lastCellDisplayedIndex to the value of the max index the table has seen. Next it will decide whether the cell it is handling should be animated or left as is. I also had to add a guard variable (of sorts), totalCellsToDisplay would act as your tables datasource array: -
(NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return totalCellsToDisplay;
}
So in your real app you would instead have
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return yourTableDataArray.count;
}
The reason I am checking the maximum number of cells being drawn is if you just have this code:
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
if (lastCellDisplayedIndex <= indexPath.row){}
then the maximum index will never go higher than the final cell, so this cell will be reanimated every-time you scroll up and down. To fix this when the indexPath = the total cells - 1 (because of zero index) then you bump the value of lastCellDisplayedIndex up so that no more cells will ever get drawn.
Finally we need to solve the issue of how many cells the table will initially draw. I'm not sure quite how this works but in my testing it would always draw 15 cells (if I returned more than 15). Anyway I implemented both my staggered load animation and fixed this problem with my loading animation function.
- (void)tableFadeInAnimation {
[_myTable reloadData];
NSArray<UITableViewCell *> *cells = _myTable.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:0.5 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
lastCellDisplayedIndex = _myTable.visibleCells.count;
NSLog(#"Table Animation Finished, lastCellDisplayed Index = %ld", lastCellDisplayedIndex);
}];
NSLog(#"end of table animation");
index += 1;
}
}
I used the completion block of the function to set the value of lastCellDisplayed equal to the number of cells that are visible. Now the table view will animate all new cells.
Hope this helps and thanks to #trungduc for the answer!

Related

UITableView Cell Insert Row Glitch

I have a table view with custom cells (all configured in a subclass using auto layout).
The cells load fine, display fine, everything is fine.
The issue is when I am inserting more rows (at the bottom). The table view is representing a feed for posts, so when the user scrolls to the bottom, before reaching the last cell, I load new posts, and then insert them into the table.
When I do this, I get this weird glitchy effect where the cells randomly come down (behind the previous cells) into place, the table view scrolls up a bit, messy.
CODE AT BOTTOM
I've uploaded a clip of me scrolling. When you see the activity indicator,
I stop scrolling. The rest of the movement is from the glitchy behavior.
Is the reason for the glitch because the cells are being drawn with auto-layout?
I would hope not, but idk..I'm not sure what to do regarding a solution. If anyone has any ideas please let me know.
FYI:
I have this (of course, since the cells are all using auto layout)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I've tried setting the estimated height to an "average" of the expected cell heights, around 65. No difference.
Update
Here's some code:
HomeViewController.m --> viewDidLoad
...
self.tableView = [KATableView.alloc initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.refreshDelegate = self;
self.tableView.estimatedRowHeight = 75;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.view addSubview:self.tableView];
// Constrains to all 4 sides of self.view
[SSLayerEffects constrainView:self.tableView toAllSidesOfView:self.view];
my table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!self.dataManager.didFinishFetchingData) return 4;
if (self.contentObjects.count == 0) return 1;
if (self.dataManager.moreToLoad) return self.contentObjects.count + 1;
return self.contentObjects.count + 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MYObject *object = self.contentObjects[indexPath.row];
SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:object.documentID];
if (!cell) {
cell = [SomeTableViewCell.alloc initWithStyle:UITableViewCellStyleDefault reuseIdentifier:object.documentID];
cell.delegate = self;
} else [cell startListeningForChanges];
return cell;
}
Here is how I am loading more data and adding it to the table view..
- (void)getHomeFeedData:(nullable void(^)(BOOL finished))completed {
[self.dataManager fetchHomeFeedDataForFeedOption:self.homeNavController.feedFilterOption completion:^(NSError * _Nullable error, NSArray<__kindof KAObject *> * _Nullable feedObjects) {
if (error != nil) {
NSLog(#"something went wrong: %#", error.localizedDescription);
if (completed) completed(NO);
return;
}
NSInteger originalCount = self.contentObjects.count;
if (self.dataManager.isFirstTimeLoading) self.contentObjects = feedObjects.mutableCopy;
else {
if (self.dataManager.isGettingNew) for (MYObject *obj in feedObjects) [self.contentObjects insertObject:obj atIndex:0];
else if (feedObjects.count > 0) [self.contentObjects addObjectsFromArray:feedObjects];
}
if (feedObjects.count > 0) {
if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
else {
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
}
} else if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
if (completed) completed(YES);
}];
}
NOTE:
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
is simply this:
- (void)insertCells:(nullable NSArray *)cells forSection:(NSInteger)section startingIndex:(NSInteger)start {
if (!cells) return;
NSMutableArray *indexPaths = #[].mutableCopy;
for (id obj in cells) {
NSInteger index = [cells indexOfObject:obj] + start;
[indexPaths addObject:[NSIndexPath indexPathForRow:index inSection:section]];
}
[self insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}
Update 2
My UITableViewCell subclass content is hidden ATM (too much difficulty in editing all my post content for the purpose of this post). I just have the subviews of each cell set to alpha = 0.f. It's just an image view, some labels, and some buttons.
No constraint issues in console, cells render perfectly when calling [self.tableView reloadData] so maybe there is something I'm doing wrong when inserting the cells?...
When you dealing with UITableView glitches:
Make sure you call UIKit API's on a main thread - turn on Main Thread checker
In your case, there might be an issue that fetchHomeFeedDataForFeedOption:completion: completion block is called not on a main thread.
Your insert is definitely wrong - all delete/insert/update/move calls for UITableView should be wrapped in beginUpdates/endUpdates
Your "load more" component at the bottom might be an issue. You need to address how it's managing contentSize/contentOffset/contentInset of table view. If it does anything but manipulating contentInset - it does wrong job.
While it's hard without debugging the whole solution, I bet options 2 & 3 are the key problems out there.

iOS Objective C : How to increase number of rows after given condition

I am very new to iOS, Living in a very small village with no help on iOS, I need help in my code where I can increase number of rows once I reach to row number 20.
I actually have more than 6000 lines, when try to show all of them in tableview, it takes lot of time, so I want to load all rows in efficient way, like increasing it like +20, +50 etc
here is my code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return a;
}
and I am trying to increase rows like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.row == 20)
{
[_myTable reloadData];
[self.myTable beginUpdates];
a= a +50;
[self.myTable endUpdates];
}
if (indexPath.row > 60)
{
[self.myTable beginUpdates];
a= a +100;
[self.myTable endUpdates];
}
}
I declared myTable as property
I am sorry if my post is looking foolish I am at very basic position and sorry for my bad English.
The numbers of rows to be displayed in a tableView depends on data available i.e., the data to be displayed in the UITableView.
Let an array "Countries" containing list of 3 countries:
NSArray *countries;
countries = #[#"India",#"USA",#"Germany",nil];
so, now the UITableView has 3 rows, so use this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return countries.count;
}
so if the countries list increased later, then it will work.
No need to increase number of rows in table manually.
If the response is from server, then use this:
NSArray *dataArray = [responseObject objectForKey:#"countriesJson"];
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return dataArray.count;
}
Sample Project:
Please check my GitHub link below:
https://github.com/k-sathireddy/TableRowCountDynamic
Note: More images are loaded if we reach a certain limit.
You have to follow below algorithm or logic,
1)First just initialize your array with the 20 objects and that array will use for the display in a UITableView as a datasource.
2)Then you need to implement the UIScrollView delegate method, - (void)scrollViewDidScroll:(UIScrollView *)aScrollView in which you can write a some code like this:
- (Void) scrollViewDidScroll: (UIScrollView *) aScrollView
{
self.tbleSerachResult.frame=self.tbleSerachResult.frame;
if (aScrollView==self.tblOutlet) {
[currentTextfield resignFirstResponder];
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
float reload_distance = 10;
if(y > h + reload_distance)
{
//add more 20 records to your existing array which used for the UITableView data source and reload the UITableView.
//Put your load more data method here...
}
}
}
3) after this, you doesn't require any other thing to maintain this. Just implement the UITableView delegate and datasource methods as normally. It will work.
Try this way, if still facing any issue let me know I will give you example code.
Hope it works for you!! Happy coding :)

Unable to get table to load in UIView

I have a ViewController in which I put another small UIView in the middle where I have placed a TableView but not sure how to display data from an array in this table, any help would be greatly appreciated. I'm trying to load the table using this code:
-(void) animateResults {
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
resultsTable.delegate = self;
resultsTable.dataSource = self;
[self.resultsTable registerClass:[resultsViewCell self] forCellReuseIdentifier:#"resultsListCell"];
[self.resultsTable reloadData];
[self.view layoutIfNeeded];
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
I am using a Custom Cell in my TableView to load an NSMutableArray. I have tried Using this code for the table view:
-(NSInteger)numberOfSectionsInTableView:(resultsViewCell *)tableView {
//Return number of sections
return 1;
}
//get number of rows by counting number of challenges
-(NSInteger)tableView:(resultsViewCell *)tableView numberOfRowsInSection:(NSInteger)section {
return questionsArray.count;
}
//setup cells in tableView
-(resultsViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//setup cell
static NSString *CellIdentifier = #"resultsListCell";
resultsViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSDictionary *results = [questionsArray objectAtIndex:indexPath.row];
NSString *resultsName = [results objectForKey:#"answers"];
BOOL correct = [[results objectForKey:#"correct"] boolValue];
if (!correct) {
cell.resultsIcon.image = [UIImage imageNamed:#"BlackIconLock.png"];
}
else{
cell.resultsIcon.image = nil;
}
cell.resultsName.text = resultsName;
return cell;
}
But the _resultsView does not load not sure why. I've got a lot of great help thus far and I really do appreciate it. I've been stuck at this point for about 2 weeks. Please help!
If you are using above xcode 7, you will use UIContainerView middle to ViewController in storyboard, its automatically Embed the UITableView to containerView, example
Try to reload data in animateResults in completion block. something like this,
completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
[self.resultsTable reloadData];
}];
I FINALLY! got this figured out My intention was to have a UITableView load inside of a UIView. I tried to drag and drop the TableView delegate and datasource on the ViewController but every time the app loaded it crashed. So I moved the table view delegate and datasource to the UIView load method here: I tried using this Method to do so.
-(void) animateResults {
//this part of my method actually worked it displayed the final score from a predifined variable.
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
//i tried using this to set my UITableView delegate & dataSource when the method was initiated instead of UIViewController load.
resultsTable.delegate = self;
resultsTable.dataSource = self;
//used this code to register UIViewCell that was nonexistent,
//i declared *CellIdentifier = #"resultsListCell" and it should have
//been *CellIdentifier = #"resultsCell". so i received an error used this code to solve it.
//duh. cell don't exist must not be the cell you created.My Bad.
[self.resultsTable registerClass:[resultsViewCell self] forCellReuseIdentifier:#"resultsListCell"];
//this code worked but after debugging i found out there was no data to load.
[self.resultsTable reloadData];
//was desperate at this point not sure why i put this in.
[self.view layoutIfNeeded];
//this also worked loads my UIView.
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
So My solution was frustrating in it's simplicity. The reason my app crashed when I tried to drag and drop the dataSource and delegate, was because of the mislabeled "static NSString *CellIdentifier", So make sure your cell identifier is correct! Not knowing this I attempted to load the UITableView dataSource and delegate in the above method. I have since modified the above method to this.
-(void) animateResults {
//populate label
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
//reload table
[self.resultsTable reloadData];
//Load View
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
This trimmed down code works because I connected the dataSource and delegate to UIViewController. Doing this loads the table when the UIViewController loads so theres no need to do so programmatically. Also, correcting the mislabeled cell identifier eliminated the need for the registerClass variable. Hope this Helps.

When UITableView is fully reloaded

I have a control that partially or fully changes content of tableView. After the change occurred, I set a flag tableViewContentHasChanged:
BOOL tableViewContentHasChanged = YES;
[self.tableView reloadData];
tableViewContentHasChanged = NO;
My problem appears in tableView:viewForHeaderInSection:; it is called after the table view is reloaded, so my flag is not effective inside that method.
In short: what's the right way to observe when the table has fully reloaded, so I could set the flag to NO? And, what am i possibly doing wrong?
I think the best way to handle this is in the data model as others mentioned but if you really need to do this, you can do the following:
According to Apple's documentation, only visible sections/cells are reloaded when you call reloadData
so you need to know when the last visible header is rendered so you set:
tableViewContentHasChanged = YES;
[self.tableView reloadData];
Then in cellForRowAtIndexPath: get the last displayed index and store it in a member variable:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//Your cell creating code here
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"TryCell"];
//Set last displayed index here
lastLoadedSectionIndex = indexPath.section;
NSLog(#"Loaded cell at %#",indexPath);
return cell;
}
That way when viewForHeaderInSection: is called you'll know which is the last header in that reload event:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
//Create or customize your view
UIView *headerView = [UIView new];
//Toggle tableViewContentHasChanged when it's the last index
if (tableViewContentHasChanged && section == lastLoadedSectionIndex) {
tableViewContentHasChanged = NO;
NSLog(#"Reload Ended");
}
return headerView;
}
Please note that this method will only work if last visible section has at least 1 row.
Hope this helps.

Animating a view inside a uitableviewcell

I have nested tableviews so that I can have a sideways scrolling tableview in each of my tableview's cells. I want to add an animation to the top row, that basically moves a view back and forth. It is working, kind of:
- (UITableViewCell *)tableView:(UITableView *)tableView willDisplayCell: (BannerCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0){
[UIView animateWithDuration:3
animations:^{
[UIView setAnimationRepeatCount: 100];
cell.handView.frame = CGRectMake(cell.handView.frame.origin.x-100, cell.handView.frame.origin.y, 32, 32);
cell.handView.alpha = 0.0f;
}
completion:^(BOOL finished){
}];
}
else cell.handView.hidden = YES;
return cell;
}
This is working except on the first time my tableview loads, the view that I want animating doesn't even show up, when I scroll to a new cell and back to the first cell then it shows up an animates fine. Not sure why it wouldn't run as expected on the first load.
Is this delegate method actually being called? Because the actual delegate method that does this has the following signature:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Your method is returning a UITableViewCell object. I don't seem to find a delgate method like this.

Resources