UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController

69,356

Solution 1

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

Solution 2

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

Solution 3

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()
    }
}

Solution 4

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.

Solution 5

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];
Share:
69,356
Timur Zanagar
Author by

Timur Zanagar

Software Craftsman based in Auckland, New Zealand.

Updated on March 31, 2020

Comments

  • Timur Zanagar
    Timur Zanagar about 4 years

    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

  • Timur Zanagar
    Timur Zanagar over 11 years
    Thanks - that achieved the effect I was after! I had to set the content offset back to 0, 0 once refreshing was finished also.
  • Dmitry Shevchenko
    Dmitry Shevchenko over 11 years
    That's strange, in my tests, endRefreshing adjusts offset as needed
  • Timur Zanagar
    Timur Zanagar over 11 years
    It did sometimes, and not others. Could be related to the UINavigationController issue? Not sure :S
  • simonthumper
    simonthumper almost 11 years
    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 :/
  • Eric Baker
    Eric Baker over 10 years
    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];
  • Fábio Oliveira
    Fábio Oliveira about 10 years
    @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.
  • ZYiOS
    ZYiOS over 9 years
    @EricBaker you can use: [self.tableView setContentOffset:CGPointMake(0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated:YES];
  • Gank
    Gank over 9 years
    This did solve my problem. But now I'm using SVPullToRefresh, how to pull it down programmatically?
  • Kyle Robson
    Kyle Robson over 9 years
    @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: github.com/samvermette/SVPullToRefresh
  • inix
    inix about 9 years
    hi,Thank you a lot,I am also curious about the magic point (0, -64) I met when debugging.
  • Igotit
    Igotit about 9 years
    @inix 20 for status bar height + 44 for navigation bar height
  • Diogo T
    Diogo T over 8 years
    Instead of -64 is better to use -self.topLayoutGuide.length
  • Koen.
    Koen. over 8 years
    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.
  • Juan Boero
    Juan Boero about 8 years
    Works for UITableView.
  • Colin Basnett
    Colin Basnett almost 8 years
    I would recommend putting sendActionsForControlEvents(UIControlEvents.ValueChanged) at the end of this function, otherwise the actual refresh logic logic will not be run.
  • JoeGalind
    JoeGalind over 7 years
    This is by far the most elegant way to do it. Including Colin Basnett's comment for better functionality. it can be used across the whole project by defining it once!
  • user1366265
    user1366265 over 7 years
    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)
  • Tudor
    Tudor over 7 years
    I've merged this with the accepted answer as it's just an update.
  • Jadamec
    Jadamec over 7 years
    I had to call beginRefreshing() in viewDidAppear instead of viewDidLoad, otherwise only the title was visible, not the spinning animation.
  • Bilal
    Bilal over 7 years
    This should fix the problem - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ [refreshControl beginRefreshing]; }); }
  • carbonr
    carbonr about 7 years
    Its the sendActions to trigger rx that makes this answer related to RxSwift incase anyone is wondering at first look
  • nmdias
    nmdias almost 7 years
    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)
  • Dru Freeman
    Dru Freeman almost 7 years
    Just a general opinion... This needs to be radar'ed if it hasn't been fixed in 11. This seems like a oversight.
  • Bassebus
    Bassebus over 6 years
    Of all the answers above, this was the only one I could get working on iOS 11.1 / xcode 9.1
  • SinisterMJ
    SinisterMJ almost 6 years
    @ColinBasnett : Adding that code (which is now sendActions(for: UIControlEvents.valueChanged)), results in an infinite loop...
  • Marc Etcheverry
    Marc Etcheverry over 5 years
    I have noted issues with using the setContentOffset:animated method, so this solution worked for me.
  • Morten Holmgaard
    Morten Holmgaard about 5 years
    Change the beginRefreshing() to the last line to fix problem with tintColor not respected on first show: stackoverflow.com/a/20383030/860488
  • francybiga
    francybiga over 4 years
    The asyncAfter is actually what makes the spinner animation work (iOS 12.3 / Xcode 10.2)
  • jegadeesh
    jegadeesh about 3 years
    calling send actions method is the only way it's working for me!
  • Mishka
    Mishka almost 3 years
    triggers infinity loop