So I was playing around my UITableView when I noticed that this code:
[self performBatchUpdates:^{
[self reloadData];
} completion:^(BOOL finished) {}];
does not lead to cellForItemAtIndexPath: being called, only sizeForItemAtIndexPath of UICollectionViewFlowLayout is called.
Does anybody have any idea why this is happening?
I know exactly what happens in reloadData but this is different from that.
From the docs of reloadData: "It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates." Perhaps this also applies to performBatchUpdates.
Related
Before, I don't know why, when I reloadData my tableVIew, it takes a very long time.
I have test Log to prove, shows the slow is not because the network:
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-began
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-end
2016-12-29 14:50:20.972 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-bigin
2016-12-29 14:50:34.870 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-end
As we see, the net-began and net-end takes very little time.
But the reloadData-bigin and reloadData-end takes a long time, so I searched the SO, what did the reloadData do? I want to know deeply, not simple answer, I searched always is simple answer, not analyse in depth.
My reloadData code:
//[self.pre_tableView reloadData];
[UIView animateWithDuration:0 animations:^{
NSLog(#"lml-info-vc-test-net-animations-reloadData-bigin");
[self.pre_tableView reloadData];
NSLog(#"lml-info-vc-test-net-animations-reloadData-end");
} completion:^(BOOL finished) {
//Do something after that...
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
}];
I use animation block, and in the completionHadler to end the refreshing.
I also searched the apple docs for reloadData
In the Discussion:
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view’s delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates.
Attention
With the attentive sentence:
It should not be called in the methods that insert or delete rows, especially within an animation block
Well! It means I should not in my UIView's animateWithDuration method to reloadData, so I replace my code to below:
[self.pre_tableView reloadData];
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
Now it is not slow any more. Very happy, I find the reason.
But, I just want to know why, why can not put reloadData method in animation block ?
And it reloadData did not fail, even takes a long time, in the animate block happens what? then it takes so many time here?
Edit -1
My additional code is below:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.pre_dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:info_TableIdentifier];
if (cell == nil) {
cell = [[LMLAgricultureTechCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:info_TableIdentifier];
}
//NSLog(#"lml-info-vc-test-cellSetModel-began");
((LMLAgricultureTechCell *)cell).model = self.pre_dataSource[indexPath.row];
//NSLog(#"lml-info-vc-test-cellSetModel-end");
((LMLAgricultureTechCell *)cell).indexPath = indexPath;
((LMLAgricultureTechCell *)cell).delegate = self;
((LMLAgricultureTechCell *)cell).photo_view.delegate = self;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// >>>>>>>>>>>>>>>>>>>>> * cell自适应 * >>>>>>>>>>>>>>>>>>>>>>>>
id model;
model = self.pre_dataSource[indexPath.row];
return [self.pre_tableView cellHeightForIndexPath:indexPath model:model keyPath:#"model" cellClass:[LMLAgricultureTechCell class] contentViewWidth:[self cellContentViewWith]];
}
UIView.animateWithDuration... methods can only animate values that are animatable. Although it sometimes feels like magic - it's not really...
reloadData is an async method that can not be handled by the animate block the be animated.
If you want to animate the transition you can either use insert/delete/moveRows or use the transitionWithView method of UIView. This method would render the view off-screen completely as it will look after all the changes you put in it's block, than animate transit between the current state of the view and the newly rendered view. The animation itself depends on the options you deliver, and you probably want to use UIViewAnimationOptionsTransitionCrossDissolve.
[UIView transitionWithView:self.pre_tableView
duration:0.3
options:UIViewAnimationOptionsTransitionCrossDissolve
animations: ^{
[self.pre_tableView reloadData];
} completion: ^(BOOL finished) {
...
}];
I have an app with a UITableView which can delete cells using a row action. However, if I do two in quick succession, the app crashes with a BAD_EXEC.
The problem is clearly timing related. I'm asking the table to do something else before it's quite finished with the old. It could be the animations or it could be the removal of cells.
Either way, calling reloadData on the tableview before I start seems to fix it. There are two problems with this solution.
Firstly, reloadData interferes with some of the niceness of the usual row removal animations. It's no biggie but I'd prefer it with all animations intact.
Secondly, I still don't fully understand what's happening.
Can any one help me understand and/or suggest a better solution?
Here's the code...
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
[self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
removeTaskFromTableView is mostly code to work out if I need to delete an entire section or a row. I've confirmed the app makes the right choice and the bug works either way so the only relevant line from the method is...
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
Edit: I have noticed that the standard delete row action provided by the system does not allow me to move that fast. There is an in-built delay which (presumably) prevents this exact problem.
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
// [self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
Or try to reload table view in main thread.
Up to iOS 8 and Xcode 6, I was able to use a UITableViewController much like Apple's Settings to configure my applications. In segue'ing from one table back to the calling table, I would reload the calling table to update data in the cells, re-select the calling row and then let the view appear animated to clear the selection.
Here's the code in the viewWillAppear method I've been using:
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:lastselected animated:YES scrollPosition:UITableViewScrollPositionNone];
[super viewWillAppear:animated];
While the cell data is updated, the nice slow disappearance of the selected row/cell doesn't happen. It is effectively cleared immediately. I've tried just about everything I can think of and can't get this working. Please help...
What has changed and how can I get that nice transition back?
Thank you.
viewWillAppear is called before your view is on the screen. So you'll not get any animations working correctly if fired from inside that method. Use viewDidAppear instead and you should see more reliable animations on transition.. Potentially use performSelector with a delay as well if you feel it's happening too quickly.
Personally as well, I'd call [super viewDidAppear:animated]; first in this case. I can't see a reason why it'd be below the other lines of code.
Also just so I'm being 100% clear... viewWillAppear is obviously ok for reloading the tableView so lets do this...
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.tableView selectRowAtIndexPath:lastSelected animated:YES scrollPosition:UITableViewScrollPositionNone];
}
Or with the perform selector delay on viewDidAppear...
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self performSelector:#selector(animateCellSelectionAtIndexPath:) withObject:lastSelected afterDelay:0.2f];
}
-(void)animateCellSelectionAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
I wanted to highlight a particular row in a tableview(even after refreshing the values in it). But each time when I refresh the table using
[tableView reloadData]
the highlighted selection is gone.
-(void) loadValues {
[tableView reloadData];
[self displaySelection];
}
When I checked the flow of execution, I found out that only after the displaySelection method, the tableView's delegate methods are executed.
I've used a variable as a flag to check for the last row in the 'cellForRow' method to solve my problem. But I really wanted to know is there any other way to check that the tableView's delegate methods are executed completely so that 'displaySelection' will be executed only after it.
Try using something like:
[tableView performSelector:#selector(reloadData) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES];
This should actually block your thread until the tableview gets reloaded.
After calling -[UICollectionView reloadData] it takes some time for cells to be displayed, so selecting an item immediately after calling reloadData does not work. Is there a way to select an item immediately after reloadData?
Along the lines of this answer I found that calling layoutIfNeeded after reloadData seemed to effectively 'flush' the reload before I do other things to the collectionView:
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
...
On the page I found this solution, some commenters indicated it didn't work for them on iOS 9, but it's been fine for me so your mileage may vary.
The Swift way:
let selected = collectionView.indexPathsForSelectedItems()
collectionView.performBatchUpdates({ [weak self] in
self?.collectionView.reloadSections(NSIndexSet(index: 0))
}) { completed -> Void in
selected?.forEach { [weak self] indexPath in
self?.collectionView.selectItemAtIndexPath(indexPath, animated: false, scrollPosition: [])
}
}
I'm handling selection of cells in collectionView: cellForItemAtIndexPath:. The problem I found was that if the cell didn't exist, simply calling selectItemAtIndexPath: animated: scrollPosition: wouldn't actually select the item.
Instead you have to do:
cell.selected = YES;
[m_collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
Don't use reloadData
Use - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion instead. The completion block is executed after animations for cell insertion/deletion etc. have completed. You can put the call to reloadData in the (void (^)(void))updates block
Apple says:
You should not call this method in the middle of animation blocks
where items are being inserted or deleted. Insertions and deletions
automatically cause the table’s data to be updated appropriately.
In fact, you should not call this method in the middle of any animation (including UICollectionView in the scrolling).
So, you can use:
[self.collectionView setContentOffset:CGPointZero animated:NO];
[self.collectionView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
or mark sure not any animation, and then call reloadData;
or
[self.collectionView performBatchUpdates:^{
//insert, delete, reload, or move operations
} completion:nil];
Hope this is helpful to you.
Make sure you're calling reloadData on the main thread. That could be the cause for the delay in your cell updates.
I handled it on the willDisplayCell colelctionView delegate. The idea: A temp variable is needed to specify the initial scrolling has performed already or not (scrollIsRequired). When the last visible cell will display, than we can scroll to the required cell and set this variable to avoid scrolling again.
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath{
//Perform any configuration
if (CGRectGetMaxX(collectionView.frame) <= CGRectGetMaxX(cell.frame)) {
// Last visible cell
if (self.scrollIsRequired) {
[self.collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:self.initiallySelectedRepresentativeVerse inSection:0] animated:YES scrollPosition:UICollectionViewScrollPositionLeft];
self.scrollIsRequired = NO;
}
}
}
It has worked for me like a charm.
This is what worked for me:
I kept a reference of the selected index path and overide the reloadData function:
override func reloadData() {
super.reloadData()
self.selectItem(at: self.selectedIndexPath, animated: false, scrollPosition: UICollectionViewScrollPosition())
}
I tried doing it using indexPathForSelectedItems, but it was creating an infinite loop on collection view load.
create a method that does the selection and call it using performSelector after calling reload e.g;
[self performSelector:#selector(selectIt) withObject:self afterDelay:0.1];