I want to delete some cells in a table view with animation one by one, at first I use code like this:
[self beginUpdates];
[self deleteRowsAtIndexPaths:removeIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self endUpdates];
There are six indexPaths in removeIndexPaths array. It works in the right way, but the animation effect is 1. the six cells be empty,2. fade the empty area.
Then I try to delete them with for/while,like this:
int removeIndexRow = indexPath.row + 1;
while (item.level < nextItemInDisplay.level)
{
NSIndexPath *removeIndexPath = [NSIndexPath indexPathForRow:removeIndexRow inSection:0];
[items removeObject:nextItemInDisplay];
[self beginUpdates];
[self deleteRowsAtIndexPaths:#[removeIndexPath] withRowAnimation:UITableViewRowAnimationFade];
NSLog(#"1");
sleep(1);
NSLog(#"2");
[self endUpdates];
}
In order to know how the function works, I use sleep and NSLog to output some flag. Then I find that the result is after outputting all the flags, the six cells been closed together, and the most unbelievable thing is that their animation like this:1.last five cells disappear with no animation, 2.the first cell be empty,3.fade the first cell empty area.
But what I want is delete cells one by one,first the first cell be empty and fade it,then the second, third...How can I solve it?
The problem is that your loop (with the calls to sleep inside it) is running on the UI thread. The UI won't update until you return control of the UI thread to the operating system, so that it can perform the necessary animations.
Try running this in a different thread, and making the calls to remove cells one by one on the UI thread. The code could look something like this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Now we're running in some thread in the background, and we can use it to
// manage the timing of removing each cell one by one.
for (int i = 0; i < 5; i++)
{
dispatch_async(dispatch_get_main_queue(), ^{
// All UIKit calls should be done from the main thread.
// Make the call to remove the table view cell here.
});
// Make a call to a sleep function that matches the cell removal
// animation duration.
// It's important that we're sleeping in a background thread so
// that we don't hold up the main thread.
[NSThread sleepForTimeInterval:0.25];
}
});
Related
I have 3 or 2 sections (depending on datasource), in my grouped UITableView. I am trying to reload the last section via:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[feedDetailTB reloadSections:[NSIndexSet indexSetWithIndex:feedDetailTB.numberOfSections-1] withRowAnimation:UITableViewRowAnimationNone];
}];
});
First of all, the footer never disappears. The data source basically keeps track of whether there are more comments or not (a simple load more functionality). In the viewForFooterInSection I simply return nil, when all the comments have been loaded.
But, as you see in the GIF, at first the loading button stays there. It is even accessible and works. When I scroll up, it vanishes and one can see it in the bottom, which is correct. But after all the comments have been reloaded, it should vanish, but sadly it stays there.
If I use reloadData it works fine. But I can't used it, since I have other sections, which I don't need to reload.
Second, there is a weird animation/flickering of the row items, even when I have used UITableViewRowAnimationNone. Not visible in the GIF
You should implement "isTheLastSection" according to your logic
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (isTheLastSection) {
return 40;
}
return 0;
}
In order to add new rows to a section, you must use the insertRowsAtIndexPaths rather than just adding new objects to data source and reloading a section.
Here's the code:
NSMutableArray *newCommentsIndexPath = [[NSMutableArray alloc] init];
for (NSInteger i = currentCount; i < (_postDetailDatasource.commentsFeedInfo.allCommentsArray.count + serverComments.count); i ++)
{
NSIndexPath *idxPath = [NSIndexPath indexPathForRow:i inSection:sectionNumber];
[newCommentsIndexPath addObject:idxPath];
}
[_postDetailDatasource.commentsFeedInfo.allCommentsArray addObjectsFromArray:serverComments];
[feedDetailTB beginUpdates];
[feedDetailTB insertRowsAtIndexPaths:newCommentsIndexPath withRowAnimation:UITableViewRowAnimationFade];
[feedDetailTB endUpdates];
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.
I have a tableview in a view that lists all the steps that will be executed. I want to show a step as marked when the step was executed. The problem I am facing is that the tableview does not refresh until the procedure executed all the steps. Consequently all the steps show as marked at the same time.
This is my function which starts running when the user presses a button:
-(void)Initialiseer {
//do something for the first step
[self.MyTableView reloadData];
[self.MyView setNeedsDisplay];
//do something for the second step
[self.MyTableView reloadData];
[self.MyView setNeedsDisplay];
//do something for the third step
[self.MyTableView reloadData];
[self.MyView setNeedsDisplay];
}
MyView is the view IBOutlet. MyTableView is the tableview IBOutlet. I also tried [self.view setNeedsDisplay] and [self. MyTableView setNeedsDisplay] but that does not work either. I also tried getting the cell from the tableview and making the changes on the cell itself. Nothing is refreshed until the procedure finished executing completely...
Can someone tell me what I am doing wrong? I am new to iOS development and I searched, read, tried... but did not find an answer so far.
I think you need to read up on the run loop and setNeedsDisplay (which I don't think does what you're expecting). Basically, the UI does not get updated until your code finishes executing. The pattern you need for displaying a complex calculation is something like this:
-(void)Initialiseer {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
//do something for the first step
dispatch_sync(dispatch_get_main_queue(), ^{
[self.MyTableView reloadData];
});
//do something for the second step
dispatch_sync(dispatch_get_main_queue(), ^{
[self.MyTableView reloadData];
});
//do something for the third step
dispatch_sync(dispatch_get_main_queue(), ^{
[self.MyTableView reloadData];
});
});
}
I have implemented an UITableView with load more functionality. The tableView loads big images from a sometimes slow server. I'm starting an URLConnection for each image and reload the indexPath corresponding to the URLConnection (saved with the connection object). The connections themselves call -reloadData on the tableView.
Now when clicking the load more button, I scroll to the first row of the new data set with position bottom. This works great and also my asynchronous loading system.
I faced the following issue: When the connection is "too fast", the tableView is reloading the data at a given indexPath while the tableView is still scrolling to the first cell of the new data set, the tableView scrolls back half the height of that cell.
This is what it should look like and what it actually does:
^^^^^^^^^^^^ should ^^^^^^^^^^^^ ^^^^^^^^^^^^^ does ^^^^^^^^^^^^^
And here is some code:
[[self tableView] beginUpdates];
for (NSMutableDictionary *post in object) {
[_dataSource addObject:post];
[[self tableView] insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:[_dataSource indexOfObject:post] inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
[[self tableView] endUpdates];
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[_dataSource indexOfObject:[object firstObject]] inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
-tableView:cellForRowAtIndexPath: starts a JWURLConnection if the object in the data source array is a string, and replaces it with an instance of UIImage in the completion block. Then it reloads the given cell:
id image = [post objectForKey:#"thumbnail_image"];
if ([image isKindOfClass:[NSString class]]) {
JWURLConnection *connection = [JWURLConnection connectionWithGETRequestToURL:[NSURL URLWithString:image] delegate:nil startImmediately:NO];
[connection setFinished:^(NSData *data, NSStringEncoding encoding) {
[post setObject:[UIImage imageWithData:data] forKey:#"thumbnail_image"];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
[cell startLoading];
[connection start];
}
else if ([image isKindOfClass:[UIImage class]]) {
[cell stopLoading];
[cell setImage:image];
}
else {
[cell setImage:nil];
}
Can I prevent the tableView from performing the -reloadRowsAtIndexPaths:withRowAnimation: calls until the tableView scrolling is done? Or can you imagine a good way to prevent this behavior?
Based on the ideas of Malte and savner (please upvote his answer as well) I could implement a solution. His answer didn't do the trick, but it was the right direction.
I had to implement -scrollViewDidEndScrollingAnimation:. I created a bool property called _autoScrolling and an NSMutableArray property for the index paths that got reloaded while scrolling. In the URLConnections finish block I did this:
if (_autoScrolling) {
if (!_indexPathsToReload) {
_indexPathsToReload = [NSMutableArray array];
}
[_indexPathsToReload addObject:indexPath];
}
else {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
And then this:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self performSelector:#selector(performRelodingAfterAutoScroll) withObject:nil afterDelay:0.0];
}
- (void)performRelodingAfterAutoScroll {
_autoScrolling = NO;
if (_indexPathsToReload) {
[[self tableView] reloadRowsAtIndexPaths:_indexPathsToReload withRowAnimation:UITableViewRowAnimationFade];
}
_indexPathsToReload = nil;
}
It took me quite a long time to find the trick with -performSelector:withObject:afterDelay: and I still don't know why I need it.
I thought the method might got called too early. So I implemented a delay of a second and tried how far I can take it down. It still works with 0.0 but not if I call the method directly or use -performSelector:withObject:.
I really hope someone can explain that.
EDIT
After revisiting this a few years later I can explain what's going on here:
Calling -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:] guarantees the call to be performed in the next runloop iteration.
So an even better or IMHO more beautiful solution would be:
[[NSOperationQueue currentQueue] addOperationWithBlock:^{
[self performRelodingAfterAutoScroll];
}];
I wrote a more detailed explanation in this answer.
Sorry i don't have enough reputation to add a comment, hence the answer to your last question in a separate answer.
-performSelector:withObject:afterDelay: with a delay of 0.0 seconds does not execute the given selector immediately but instead performs it after the current Runloop Cycle finishes and after the given delay.
Where as -performSelector:withObject: is added to and executed in the current Runloop Cycle. Which is the same as directly calling the method.
Therefore using -performSelector:withObject:afterDelay: the UI will get updated in the current Runloop Cycle i.e in this case the scrolling animation can finish, before your selector is performed(and reloads the UI once more).
Source: Apple Dev Docs and this Thread Answer
You can use the UIScrollViewDelegate protocols (which you get for free using UITableViewDelegate) and utilize the -scrollViewDidScroll or -scrollViewWillBeginDragging: methods to detect scrolling has started or stopped. Work with those callbacks to control when you want to load/stop loading cell data.
i have an iOS app with UITabbarController, and in one tab view there is a UITableViewController, when i switch tab i what reload the visible cell row because the row contain a UILabel with time and i want refresh the time with the passage of time, but when i tap the tab with the UITableView, i use this code:
NSArray *visible = [self.tableView indexPathsForVisibleRows];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:visible withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
and i call it in ViewWillAppear, and with this code takes a moment to open, is not immediate, and with out it is immediate. So how i can refresh that rows without block the UI?
Putting the code in ViewWillAppear: means it gets run before the view has appeared. Since you want animations that the user will see, it makes sense to move this code into ViewDidAppear
Don't forget to call [super viewDidAppear]!