UITableView prevent reloading while scrolling - ios

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.

Related

Asynchronous loading of data from multiple api

Loading data from one api request and storing it in one array (suppose n object coming in response json object),
and another api request takes argument from first api request and loads status of n objects.
1) first api request will load n objects and display it into table:
dispatch_queue_t loadDataQueue = dispatch_queue_create("loadDataQueue",NULL);
dispatch_async(loadDataQueue, ^{
// Perform long running process
[self loadData];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
[tableView reloadData];
[self hideActivityView];
});
});
2) now I am calling loadstatus method, it takes parameter from objectatindex and loads status data for objectatindex. So this method calling n times in cellForRowAtIndexPath method.
dispatch_queue_t loadStatusQueue = dispatch_queue_create("loadStatusQueue",NULL);
dispatch_async(loadStatusQueue, ^{
// Perform long running process
[self loadStatus];
dispatch_async(dispatch_get_main_queue(), ^
// Update the UI
[tableView reloadData];
});
});
This updates one row at a time. So reloading table n times.
It takes so time to load status of all objects.
Some time hanging problem occured.
Can somebody please provide efficient solution for this or other way to do this?
The info in your question was a bit unclear ( especially about objectAtIndex... Does it mean that in cellForRowAtIndexPath you get the cell's info, then based on that you send another async request to get it's status?)
It might not be enough to tell all reasons could make your app slow, but I could say it's not a good idea to reload the whole table just to update one cell. Also I think you should call loadData to get full data list of basic info first, then call loadStatus for 'visible' cells.
I guess you know how to store the loaded data/status in arrays to prevent refetching data. So the example below is possibly what you could adopt to improve the performance:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"ReusableCell" forIndexPath:indexPath];
[cell configureData:self.loadedData[indexPath.row]];
if (self.loadedStatus[indexPath.row]) {
// If the status has been loaded then
[cell configureStatus:self.loadedStatus[indexPath.row]];
} else {
dispatch_queue_t loadStatusQueue = dispatch_queue_create("loadStatusQueue",NULL);
__weak __typeof(self) weakSelf = self;
dispatch_async(loadStatusQueue, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Perform your long running process here
// Eg: [strongSelf loadStatusForIndex:indexPath.row];
UITableViewCell *blockCell = (UITableViewCell *)[strongSelf.tableView cellForRowAtIndexPath:indexPath];
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}
});
....
}
return cell;
}

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.

Tableview reload with setNeedsDisplay not working

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

UITableViewCell not updating after location callback until scroll or selection

I am trying to update a UITableViewCell as soon as the user's location is found and reverse-geocoded.
From reading lots of other answers to similar questions, it seems the tableview reload must occur on the main thread, which I have tried without any success.
All the location data gets retrieved correctly, and is correctly added to the core data object, but the tableview cell simply is not updating until the user scrolls or the cell is selected, at which point the cell is correctly updated from that point on.
Here is a selection from my code - does anyone know why the tableview cell isn't updating right away?
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations // Delegate callback method.
{
// Correctly gets currentLocation coordinates.
...
CLLocation *currentLocation = [locations lastObject];
...
// Reverse-geocode the coordinates (find physical address):
[geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error == nil && [placemarks count] > 0) {
CLPlacemark *placemark = [placemarks lastObject]; // Correctly gets placemark.
// Correctly adds placemark to core data object.
...
...
// Reload TableView:
// [self.tableView reloadData]; //Tried this, didn't work, since not on main thread.
// [self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO]; //Doesn't work.
// [self performSelector:(#selector(refreshDisplay)) withObject:nil afterDelay:0.5]; //Doesn't work.
[self performSelectorOnMainThread:#selector(refreshDisplay) withObject:nil waitUntilDone:NO]; //Doesn't work.
}
}];
}
- (void)refreshDisplay {
[_tableView reloadData];
}
Again, the underlying logic is working, since the data gets added correctly and eventually shows up on the cell, but not until user scroll or selection. I can't imagine why this doesn't refresh the tableviewcell right away. Does anyone have any idea?
UPDATE: My Solution
The missing piece was adding [cell layoutSubviews] just after the cell is created/dequeued. The detailTextLabel now updates correctly. Apparently this may be related to an iOS8 bug that doesn't update the detailText if it starts out nil (i.e. no content), and layoutSubviews makes it so it is not nil, by initializing all the subviews of the cell (as far as I understand). I got this suggestion from here:
ios 8 UITableViewCell detail text not correctly updating
The accepted answer also helped me figure out this missing piece.
You should get a reference to the cell that you want to update
Ex. UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:theRow inSection:theSection]];
Then [[cell textLabel] setText:theLocation];
If your updating multiple cells just get multiple references of the cells you want to update and update them accordingly.
Typically anything dealing with UI components that need updating should be done on the main thread. I have come across this issue before but if you want to have a solution dealing with threading, what you have done seems like it should work.
Here is the GCD solution:
dispatch_async(dispatch_get_main_queue(), ^{
[self.table reloadData];
});
But according to what you've done, it shouldn't help... It is basically the same thing just with GCD. Hopefully the solution I gave you at the beginning works...
EDIT
Within your - (UITableViewCell *)tableView:(UITableView *)tableView cellForNextPageAtIndexPath:(NSIndexPath *)indexPath
cell = [[PFTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
Did you set the style to UITableViewCellStyleDefault?
It seems that tableview cell is not completely visible in the view.In that case You can use [cell setNeedsDisplay]; for example: dispatch_async(dispatch_get_main_queue(), ^{ [cell setNeedsDisplay];
[cell.contentView addSubview:yourView];
});
Hope this works for you.
I had the same problem. None of the answers here worked for me. I finally searched thru every aspect of the nib and saw that my controls (buttons and the table view) were not connected to outlets even though in the .h file the circle indicator was selected. But in the connections inspector they were not connected to outlets. So had to delete all the .h file controls and reconnect and low and behold everything works.

iOS: Updating tableViewCell infinite loop

This is the problem: I update my tableview after saving and fetching an thumbnail to Core Data and then I tell the cell to update itself - so it can show a thumbnail when the image has been loaded into Core Data. I Use two different threads as Core Data is not threadsafe and all GUI elements of course needs to happen in the main thread.
But this whole method just keep looping forever, and what is causing it is when I reload the thread:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Why? and how do I fix this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Photo"];
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = photo.title;
cell.detailTextLabel.text = photo.subtitle;
NSLog(#"Context %#", self.photographer.managedObjectContext);
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
return cell;
}
The infinite loop is caused by calling the cellForRowAtIndexPath: again after the fetch is loaded and the reload on a certain cell is called.
A reload will force the cellForRowAtIndexPath: to be called again... and in your case again and again to infinity.
The solution is simple... Do not reload your cell in the cellForRowAtIndexPath but rather in a callback-method of the fetchrequest. Then reload it there rather then the creation of the cell.
Rather do not load the image inside the cellForRowAtIndexpath: at all.
Whenever your table is instantiated make a method that loops over your datasource and get the respective cell for each item. Then load an image for each cell you deem needed. And reload the cell whenever the fetching of the item is done (for instance a callback method).
If you do want the image to be loaded inside the creation of the cell as you have done now (Although I don't think that is the proper way to do it). You can surround the whole performBlock: with an if-statement checking if the image has already been set or not.
As already been said by others, there should be no need to call reloadRowsAtIndexPaths when the image has been loaded. Assigning a new image to cell.imageView.image is sufficient.
But there is another problem with your code. I assume that self.photographer.managedObjectContext is a managed object context of the "private concurrency type", so that performBlock executes on a background thread. (Otherwise there would be no need to use dispatch_async(dispatch_get_main_queue(), ...).)
So performBlock: executes the code asynchronously, on a background thread. (Which is good because the UI is not blocked.) But when the image has been fetched after some time, the cell might have been reused for a different row. (This happens if you scroll the table view so that the row becomes invisible while the image is fetched.)
Therefore, you have to check if the cell is still at the same position in the tableview:
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
if ([[tableView indexPathForCell:cell] isEqualTo:indexPath]) {
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
}
});
}];
I'd replace the following code:
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
With:
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[blockCell setNeedsLayout];
This solves the issue of reloading recursively, as well as the possibility that cell has gotten reused in the interim. I'd also check for whether or not you've loaded the photo data already and not update if so.
Since you are using an NSFetchedResultsController though, you may be better off going with Totomus Maximus' answer, if the delegate callbacks are already informing you when the photo data updates. This way just updates the image view and wouldn't update any other information that the Photo update method could change.

Resources