131

I've setup a UIRefreshControl in my UITableViewController (which is inside a UINavigationController) and it works as expected (i.e. pull down fires the correct event). However, if I programmatically invoke the beginRefreshing instance method on the refresh control like:

[self.refreshControl beginRefreshing];

Nothing happens. It should animate down and show the spinner. The endRefreshing method works properly when I call that after the refresh.

I whipped up a basic prototype project with this behavior and it works properly when my UITableViewController is added directly to application delegate's root view controller, e.g:

self.viewController = tableViewController;
self.window.rootViewController = self.viewController;

But if I add the tableViewController to a UINavigationController first, then add the navigation controller as the rootViewController, the beginRefreshing method no longer works. E.g.

UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:tableViewController];
self.viewController = navController;
self.window.rootViewController = self.viewController;

My feeling is this has something to do with the nested view hierarchies within the navigation controller not playing nice with the refresher control - any suggestions?

Thanks

shim
  • 9,289
  • 12
  • 69
  • 108
wows
  • 10,687
  • 7
  • 27
  • 27

15 Answers15

204

It seems that if you start refreshing programmatically, you have to scroll the table view yourself, say, by changing contentoffset

[self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:YES];

I would guess the reason for this is that it could be undesirable to scroll to the refresh control when user is in the middle/bottom of the table view?

Swift 2.2 version by @muhasturk

self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)

In a nutshell, to keep this portable add this extension

UIRefreshControl+ProgramaticallyBeginRefresh.swift

extension UIRefreshControl {
    func programaticallyBeginRefreshing(in tableView: UITableView) {
        beginRefreshing()
        let offsetPoint = CGPoint.init(x: 0, y: -frame.size.height)
        tableView.setContentOffset(offsetPoint, animated: true)        
    }
}
Ratul Sharker
  • 7,484
  • 4
  • 35
  • 44
Dmitry Shevchenko
  • 31,814
  • 10
  • 56
  • 62
  • Thanks - that achieved the effect I was after! I had to set the content offset back to 0, 0 once refreshing was finished also. – wows Feb 06 '13 at 01:06
  • 6
    That's strange, in my tests, endRefreshing adjusts offset as needed – Dmitry Shevchenko Feb 06 '13 at 01:20
  • It did sometimes, and not others. Could be related to the UINavigationController issue? Not sure :S – wows Feb 06 '13 at 01:45
  • 9
    BTW, if you're using auto layout, you can replace the line in the answer with this: [self.tableView setContentOffset:CGPointMake(0, -self.topLayoutGuide.length) animated:YES]; – Eric Baker Oct 30 '13 at 00:06
  • 4
    @EricBaker I believe that won't do. Not all UITableViewControllers show navigation bars. This would lead to a topLayoutGuide of length 20 and an offset too small. – Fábio Oliveira Feb 17 '14 at 14:44
  • 3
    @EricBaker you can use: [self.tableView setContentOffset:CGPointMake(0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated:YES]; – ZYiOS Oct 30 '14 at 07:07
  • I had to call beginRefreshing() in viewDidAppear instead of viewDidLoad, otherwise only the title was visible, not the spinning animation. – Jadamec Nov 10 '16 at 11:01
  • This should fix the problem - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ [refreshControl beginRefreshing]; }); } – Bilal Nov 21 '16 at 07:17
  • Just a general opinion... This needs to be radar'ed if it hasn't been fixed in 11. This seems like a oversight. – Dru Freeman Jul 18 '17 at 15:26
  • Change the beginRefreshing() to the last line to fix problem with tintColor not respected on first show: https://stackoverflow.com/a/20383030/860488 – Morten Holmgaard Mar 21 '19 at 13:54
80

UITableViewController has automaticallyAdjustsScrollViewInsets property after iOS 7. The table view may already have contentOffset, usually (0, -64).

So the right way to show refreshControl after programmingly begin refreshing is adding refreshControl's height to existing contentOffset.

 [self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
Igotit
  • 1,056
  • 9
  • 5
35

Here's a Swift extension using the strategies described above.

extension UIRefreshControl {
    func beginRefreshingManually() {
        if let scrollView = superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: true)
        }
        beginRefreshing()
    }
}
Chris Wagner
  • 20,773
  • 8
  • 74
  • 95
15

None of the other answers worked for me. They would cause the spinner to show and spin, but the refresh action itself would never happen. This works:

id target = self;
SEL selector = @selector(example);
// Assuming at some point prior to triggering the refresh, you call the following line:
[self.refreshControl addTarget:target action:selector forControlEvents:UIControlEventValueChanged];

// This line makes the spinner start spinning
[self.refreshControl beginRefreshing];
// This line makes the spinner visible by pushing the table view/collection view down
[self.tableView setContentOffset:CGPointMake(0, -1.0f * self.refreshControl.frame.size.height) animated:YES];
// This line is what actually triggers the refresh action/selector
[self.refreshControl sendActionsForControlEvents:UIControlEventValueChanged];

Note, this example uses a table view, but it could just as well have been a collection view.

Kyle Robson
  • 3,060
  • 24
  • 20
  • This did solve my problem. But now I'm using `SVPullToRefresh`, how to pull it down programmatically? – Gank Dec 21 '14 at 09:37
  • 1
    @Gank I've never used SVPullToRefresh. Have you tried reading their docs? It seems quite obvious based on the docs that it can be pulled down programmatically: "If you’d like to programmatically trigger the refresh (for instance in viewDidAppear:), you can do so with: `[tableView triggerPullToRefresh];`" See: https://github.com/samvermette/SVPullToRefresh – Kyle Robson Dec 21 '14 at 16:18
  • 2
    Perfect! It was acting strange for me with the animation though, so I simply replaced it with `scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)` – user1366265 Sep 15 '16 at 17:25
13

The already mentioned approach:

[self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];

would make the spinner visible. But it wouldn't animate. The one thing I changed is the order of these two methods and everything worked:

[self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
[self.refreshControl beginRefreshing];
AndroC
  • 4,758
  • 2
  • 46
  • 69
11

For Swift 4/4.1

A mix of existing answer do the job for me:

refreshControl.beginRefreshing()
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)

Hope this helps!

Isaac Bosca
  • 1,588
  • 1
  • 15
  • 34
8

See also this question

UIRefreshControl not showing spiny when calling beginRefreshing and contentOffset is 0

It looks like a bug to me, because it only occures when the contentOffset property of the tableView is 0

I fixed that with the following code (method for the UITableViewController) :

- (void)beginRefreshingTableView {

    [self.refreshControl beginRefreshing];

    if (self.tableView.contentOffset.y == 0) {

        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){

            self.tableView.contentOffset = CGPointMake(0, -self.refreshControl.frame.size.height);

        } completion:^(BOOL finished){

        }];

    }
}
Community
  • 1
  • 1
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
  • 1
    This is not true, I have two different UIViewControllers both of which have a contentOffset of 0 upon viewDidLoad and one of them correctly pulls down the refreshControl upon calling [self.refreshControl beginRefreshing] and the other does not :/ – simonthumper Jul 11 '13 at 11:21
  • Documentation doesn't say anything about displaying the control on `beginRefreshing`, only that its state changes. As I see it, it is to prevent to initiate the refresh action twice, so that might a programmatically called refresh would still be running, a user initiated action won't start another. – Koen. Dec 25 '15 at 12:07
  • I have noted issues with using the setContentOffset:animated method, so this solution worked for me. – Marc Etcheverry Jan 28 '19 at 22:21
7

Here is Swift 3 and later extension that shows spinner as well as animate it.

import UIKit
extension UIRefreshControl {

func beginRefreshingWithAnimation() {

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {

        if let scrollView = self.superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - self.frame.height), animated: true)
          }
        self.beginRefreshing()
      }
   }
}
Ghulam Rasool
  • 3,996
  • 2
  • 27
  • 40
5

It's works perfect to me:

Swift 3:

self.tableView.setContentOffset(CGPoint(x: 0, y: -self.refreshControl!.frame.size.height - self.topLayoutGuide.length), animated: true)
Meilbn
  • 582
  • 7
  • 8
5

For Swift 5, for me the only thing missing was to call refreshControl.sendActions(.valueChanged). I made an extension to make it more cleaner.

extension UIRefreshControl {

    func beginRefreshingManually() {
        if let scrollView = superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: false)
        }
        beginRefreshing()
        sendActions(for: .valueChanged)
    }

}
LukasHromadnik
  • 387
  • 4
  • 8
4

In addition to @Dymitry Shevchenko solution.

I found nice workaround to this issue. You can create extension to UIRefreshControl that overwrites method:

// Adds code forgotten by Apple, that changes content offset of parent scroll view (table view).
- (void)beginRefreshing
{
    [super beginRefreshing];

    if ([self.superview isKindOfClass:[UIScrollView class]]) {
        UIScrollView *view = (UIScrollView *)self.superview;
        [view setContentOffset:CGPointMake(0, view.contentOffset.y - self.frame.size.height) animated:YES];
    }
}

You can use new class by setting custom class in Identity Inspector for refresh control in Interface Builder.

lyzkov
  • 1,307
  • 1
  • 9
  • 11
4

Fort Swift 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
muhasturk
  • 2,534
  • 20
  • 16
4

If you use Rxswift for swift 3.1, can use below:

func manualRefresh() {
    if let refreshControl = self.tableView.refreshControl {
        self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.height), animated: true)
        self.tableView.refreshControl?.beginRefreshing()
        self.tableView.refreshControl?.sendActions(for: .valueChanged)
    }
}

This work for swift 3.1, iOS 10.

jkyin
  • 168
  • 1
  • 12
  • 1
    Its the `sendActions` to trigger `rx` that makes this answer related to `RxSwift` incase anyone is wondering at first look – carbonr May 09 '17 at 11:27
  • I had to use the refreshControl instance from the tableViewController, not the tableView. Also, that setContentOffset didn't work for me targeting iOS10. This one works however: `self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)` – nmdias Jul 12 '17 at 10:16
2

tested on Swift 5

use this in viewDidLoad()

fileprivate func showRefreshLoader() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentOffset.y - (self.refreshControl.frame.size.height)), animated: true)
        self.refreshControl.beginRefreshing() 
    }
}
Pramodya Abeysinghe
  • 1,098
  • 17
  • 13
1

I use the same technique for show user "data is update" visual sign. A result user bring app from background and feeds/lists will be update with UI like users pull tables to refresh himself. My version contain 3 things

1) Who send "wake up"

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationHaveToResetAllPages object:nil];
}

2) Observer in UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(forceUpdateData) name:kNotificationHaveToWakeUp:nil];
}

3)The protocol

#pragma mark - ForcedDataUpdateProtocol

- (void)forceUpdateData {
    self.tableView.contentOffset = CGPointZero;

    if (self.refreshControl) {
        [self.refreshControl beginRefreshing];
        [self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:YES];
        [self.refreshControl performSelector:@selector(endRefreshing) withObject:nil afterDelay:1];
    }
}

result

WINSergey
  • 1,977
  • 27
  • 39