UITableView Flickering with UIRefreshControl - ios

I have aUITableView withUIRefreshControl defined inside the viewDidLoad: method of aUIViewController as below:
- (void)viewDidLoad {
[super viewDidLoad];
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:#selector(refreshMessages) forControlEvents:UIControlEventValueChanged];
UITableViewController *tableViewController = [[UITableViewController alloc] init];
tableViewController.tableView = self.messagesTableView;
tableViewController.refreshControl = self.refreshControl;
}
- (void)refreshMessages {
//load data items
if ([self.refreshControl isRefreshing]) {
[self.refreshControl endRefreshing];
}
[self.messagesTableView reloadData];
}
I use self sizing cell to update the cell data. On re-loading the data theUITableView flickers. Is there a way to avoid this?
Edit
I have linked the video file for a sample demo. The UITableView on reload data, flickers. I use autolayout to update the dynamic cell heights.
Here is a sample code that can recreate this issue. The UITableView should be pulled to refresh. The table view is reloaded on the main queue. The cell are being resized with autolayout.

I got the same issue today and I managed to solve the problem using CATransaction, here is a code snippet in Swift, executed within a UITableViewController :
CATransaction.begin()
CATransaction.setCompletionBlock { () -> Void in
/*wait for endRefreshing animation to complete
before reloadData so table view does not flicker to top
then continue endRefreshing animation */
self.tableView.reloadData()
}
self.refreshControl!.endRefreshing()
CATransaction.commit()

The problem is that you are calling -reloadData and -endRefreshing together. Both want to update the view. You need to separate them a bit.
One of these options will work, depending on where you want to see the cells update:
[self.tableView reloadData];
[self.refreshControl performSelector:#selector(endRefreshing) withObject:nil afterDelay:0.5];
[self.refreshControl endRefreshing];
[self.tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0.5];
Probably you need less delay with the first option. Even 0.0 seconds seemed to work in your example code. That's enough to get the -endRefreshing animation on the next screen refresh.

Swift
You may want to use
extendedLayoutIncludesOpaqueBars = true
in your ViewController in order to have correct UIRefreshControl animation.

I guess the issue is using estimatedHeight & UITableViewAutomaticDimension along with uitableview's reloadData. This has been reported here
The delay would work but its still not the right way to achieve it unless you want a hackish work around.

May be calling reloadData and endRefreshing could have caused the flicker to take place so what i did was modified your code a bit and removed the dispatch block which cleared off the flick
- (void)refreshMessages{
self.messages = [NSMutableArray new];
[self loadData];
[self.tableView reloadData];
[self.refreshControl performSelector:#selector(endRefreshing) withObject:nil afterDelay:0.5];
// double delayInSeconds = 0.5;
// dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
// dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// [self.refreshControl endRefreshing];
// });
}

Have you tried?
[self.messagesTableView beginUpdates];
[self.messagesTableView reloadSections:[NSIndexSet indexSetWithIndex:0/*target section*/] withRowAnimation:UITableViewRowAnimationNone];
[self.messagesTableView endUpdates];
instead of:
[self.messagesTableView reloadData];

-(void)refreshMessages{
self.messages = [NSMutableArray new];
[self loadData];
[self.tableView reloadData];
[self.refreshControl endRefreshing];
}

Related

request for index path for global index 1793701674059 when there are only 6 items in the collection view

UICollectionView crash when performBatchUpdates after reload data.I don't perform any delete or add operation,only refresh data after net request success.
code like this
- (void)reloadData:(NSArray *)dataArray {
self.dataSourceArray = dataArray;
[_collectionView reloadData];
[_collectionView performBatchUpdates:nil completion:^(BOOL finished) {
if ([_collectionView numberOfItemsInSection:0] > 0) {
[_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
}
}];
}
function reloadData maybe call twice consecutive and fast,everytime use new data reload collection view.
i think somewhere use incorrect with collection view,but i can't find it.

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

tableView reloadData and deselectRowAtIndexPath

I am using the following code to deselect a selected table view cell when returning back to the table view in -viewWillAppear:animated.
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
I also need to reload the table view's data in this case but when you do that it clears the selected state of the selected cell so you don't see any fade animation.
Is there a way to reload the table data and also preserve the selected state to create the deselect animation?
After several attempts, I've found something that works. You need to set the deselection to occur after a "delay" (of 0 seconds) in order to make sure it happens on the next draw cycle and gets animated properly.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow;
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[self performSelector:#selector(deselectRow) withObject:nil afterDelay:0];
}
- (void)deselectRow
{
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
}
Try this in your viewDidLoad:
[self setClearsSelectionOnViewWillAppear:NO];
The obvious solution suggested by #user2970476 seems to work fine on iOS 7. For iOS 8 I slightly modified #Stonz2's answer to use blocks
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^(void) { // necessary for iOS 8
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
});
}
I also had to set self.clearsSelectionOnViewWillAppear = NO; in viewDidLoad because the IB setting got ignored.
You will need to reload your table view first, select the row you want to indicate then perform the deselect animation. The issue you are having is your order of operation is incorrect.
You can save the current selection with
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:selectedRow animated:NO scrollPosition:UITableViewScrollPositionNone];

Reloading Table and Clearing Cell Row Selection

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

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.

Resources