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.
Related
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!
I have list of complex UIViewControllers. This list should be displayed in vertical form. I'm wondering how can I display them. I tried UIPageViewController, but it's showing just one child controller. I need to show them as many as can fit on screen (like UITableView or UIScrollView). I cannot use UITableView, because it doesn't support nested UIViewControllers. So do I have to use UIScrollView and implement own releasing mechanism for child controllers or is there any other way?
You can use UITableView:
in tableViewDelegate put
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//calc cell height
return height;
}
than in this method do like there:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:COMMON_CELL_IDENTIFIER];
[self addChildViewController:viewControllers[indexPath.row]];
[cell.contentView addSubview:[viewControllers[indexPath.row] view]];
return cell;
}
My UITableView shows answers for questions.
To switch questions I apply CATransition to tableView and call reloadData method.
In case if user did select one row, went to another question and then returned back to the first question, I want the chosen answer to be selected.
I call
[tableView selectRowAtIndexPath:indexPath
animated:YES
scrollPosition:UITableViewScrollPositionNone];
in delegate method
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
But when answers appear after reloading UITableView, there are no selected cells. If I move cell out of the screen and then move it back, it becomes selected.
you need to implement following UITableView delegate method to perform any action on selection of cell
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// [self doSomethingWithRowAtIndexPath:indexPath];
// write your action on cell here
}
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.
I have a UITableView with some delegate methods.During load for the first time it's all ok, but during the rotation I saw that the method cellForRowAtIndexPath doesn't recall. Why?
My delegate method are:
-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
//.....
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
//......
}
- (UITableViewCell *)tableView:(UITableView *)aTableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//...This method is not called during the rotation of the device...
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//.......
}
You need to refresh table data manually after the rotation occurs using [tableView reloadData] within shouldAutorotateToInterfaceOrientation method.
That method is only called when the cell is about to become visible on the screen and the table requests it. Once it is in the view, it will not be called again until it has scrolled out of view and then back.
You have 3 options for handling the rotation:
Use AutoLayout
Use Springs & Struts
Override layoutSubviews (which will get called on a rotation)
What you choose will depend on how complex your cell layout is. Also, make sure the table resizes on the rotation, otherwise the cells won't see a size change.