Tableview reload with setNeedsDisplay not working - ios

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];
});
});
}

Related

Why UITableView reloadData can not in animate block?

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) {
...
}];

UITableView crash if I delete rows too fast

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.

UITableView prevent reloading while scrolling

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.

Execute a method right after tableview delegate methods

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.

iOS UITableView delete cells with animation one by one

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];
}
});

Resources