22

When my data has finished loading and my tableView has been reloaded, I call endRefreshing on my refresh control, it then 'jumps' from its loading state and disappears - how can I implement a smooth animation that slides the refresh control away when it is complete?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Halpo
  • 2,982
  • 3
  • 25
  • 54

7 Answers7

32

I'm adding a new answer since the only one is not very precise in explanation. And this would probably be too much for comments.

The solution by Halpo is correct. But the reason mentioned is wrong.

Calling -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:] guarantees the call to be performed in the next runloop iteration.

So this does not work, because there is a small delay, but you're moving it to the next loop.

Actually you sh/could decrease the delay to 0.0 and it would still work. Also, a more correct (in terms of using the object meant to achieve something, not better in outcome) implementation is possible.

Here are the various code snippets for that:

// SOLUTION I

[[self tableView] reloadData];
[[self refreshControl] performSelector:@selector(endRefreshing) withObject:nil afterDelay:0.0];

// SOLUTION II (semantically correct)

[[self tableView] reloadData];
[[NSOperationQueue currentQueue] addOperationWithBlock:^{
    [[self refreshControl] endRefreshing];
}];

// SOLUTION III (GCD)

dispatch_async(dispatch_get_main_queue(), ^{
    [[self refreshControl] endRefreshing];
}];

EDIT

As johann-fradj pointed out, you could also use GCD. I do like GCD a lot, but I think, solution II is most descriptive about whats going on. Also, GCD is not very Objective-C-ish, so I'd personally rather stick to my solution.

Community
  • 1
  • 1
Julian F. Weinert
  • 7,474
  • 7
  • 59
  • 107
  • This should be the right answer. Also you could simply dispatch_async in the main queue. – Johann Fradj Feb 03 '16 at 14:44
  • 1
    Funny enough Solution 1 worked but solution 2 didn't. Dispatch_async on the main queue didn't either. – donkey Mar 16 '16 at 19:35
  • @JohannFradj thanks for your input, I added it to my answer. But as I point out in my comment, I wouldn't use it here ;-) – Julian F. Weinert Mar 18 '16 at 16:18
  • 1
    The GCD solution had been working for me but then stopped. Even solution II results in choppy animations. That said, committing a CATransaction around reloading the tableview with `[refreshControl endRefreshing]` in its completion block results in a very smooth animation. I'll admit that this feels like a hack since to my knowledge `[tableView reloadData]` isn't animatable, but this is the only solution that works in my particular situation. – Chris Oct 04 '16 at 16:38
  • Strange that it stopped working. Interesting approach with the transaction, I'll definitely try it out to see if I like it. What does "stopped working" mean? Are you talking about a specific iOS update? – Julian F. Weinert Oct 04 '16 at 19:21
  • #1 worked for me: `self.refreshControl.perform(#selector(UIRefreshControl.endRefreshing), with: nil, afterDelay: 0)` – Jordan H Aug 28 '19 at 17:17
27

Fixed the problem - I added a tiny delay to endRefresh after reloading data:

[self.tableView reloadData];
[self.refreshControl performSelector:@selector(endRefreshing) withObject:nil afterDelay:0.05];
Halpo
  • 2,982
  • 3
  • 25
  • 54
  • "endRefreshing" Must be explicitly called when the refreshing has completed – alexmorhun Feb 23 '17 at 16:51
  • I say "endRefreshing" must be called once the datasource has been loaded with data. But it should be called before reloading the TableView or CollectionView otherwise it gives a weird buggy and stutter effect. – Anjan Biswas Nov 16 '17 at 02:46
  • 1
    In Swift: `self.refreshControl.perform(#selector(UIRefreshControl.endRefreshing), with: nil, afterDelay: 0)` – Jordan H Aug 28 '19 at 17:17
5

Swift 4 solution

    tableView.reloadData()
    if refreshControl?.isRefreshing == true {
        DispatchQueue.main.async {
            self.refreshControl?.endRefreshing()
        }
    }

Other solutions (delay, using CATransaction, ...) are more complex and don't give any visible advantage

coldfire
  • 946
  • 10
  • 8
3

Try wrapping the call to endRefreshing() in a UIView animation block:

I've run into this issue as well. For endRefreshing() The documentation states:

UIView.animateWithDuration(0.5) {
    self.refreshControl.endRefreshing()
}

The documentation states:

If animations are also enabled, the control is hidden using an animation.

Eytan
  • 1,825
  • 17
  • 24
0

One of the reasons can be that you refresh your table view after the endRefresh is called. For example, my calls used to look like this:

[self.refreshControl beginRefreshing];
[self.network getThings:^(id things, NSError *error) {
    [self.refreshControl endRefreshing];

    if (error) {
        ...
    } else {
        self.things = things;
        [self.tableView reloadData];
    }
}];

Which is bad, because the table updates itself while the endRefresh animation runs. So the code done right looks like this:

[self.refreshControl beginRefreshing];
[self.network getThings:^(id things, NSError *error) {
    if (error) {
        [self.refreshControl endRefreshing];
        ...
    } else {
        self.things = things;
        [self.tableView reloadData];
        [self.refreshControl endRefreshing];
    }
}];
gklka
  • 2,459
  • 1
  • 26
  • 53
  • 1
    Just to give a little "lesson" in code design: Put the `endRefreshing` call AFTER the else closes. You'll keep your code "smaller" and a lot more readable. Also, if you're only interested in error _reasons_ during develoment (which you should not), you could simply invert the if and get away with even less code. – Julian F. Weinert Jul 05 '16 at 00:11
  • 1
    You are right, Julian. The copied code fragment looks like this, because the "..." needed to run after the endRefreshing animation :) In this particular example the method you suggested would be better. – gklka Jul 05 '16 at 07:36
0

The solution for me was to run refreshControl?.endRefreshing() before the call to tableView.reloadData():

refreshControl?.endRefreshing()
tableView.reloadData()

When the call to endRefreshing() was after the call to reloadData(), the TableView would jump whenever the data was reloaded.

Forest Katsch
  • 1,485
  • 2
  • 15
  • 26
0

I found that calling endRefreshing() around the same time the tableView finishes its reload causes the bouncing/flashing glitch. I tried the other options mentioned in other answers with OperationQueue and performSelector but it did not provide me the control or smoothness I was seeing in other apps. Instead I opted for this solution updated for Swift and my specific scenario:

UIView.animate(withDuration: self.refreshControl.isRefreshing ? 0.5 : 0) {
    self.refreshControl.endRefreshing()
} completion: { finished in
    self.tableView.reloadData()
}

With this we can make sure that we end the refreshing from the refreshControl before the tableView reloading ever happens. That way we can ensure the separation of the two actions with no conflict in animation.

Julio
  • 483
  • 1
  • 4
  • 17