UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController
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];
Timur Zanagar
Software Craftsman based in Auckland, New Zealand.
Updated on March 31, 2020Comments
-
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 therootViewController
, thebeginRefreshing
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 over 11 yearsThanks - 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 over 11 yearsThat's strange, in my tests, endRefreshing adjusts offset as needed
-
Timur Zanagar over 11 yearsIt did sometimes, and not others. Could be related to the UINavigationController issue? Not sure :S
-
simonthumper almost 11 yearsThis 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 over 10 yearsBTW, 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 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 over 9 years@EricBaker you can use: [self.tableView setContentOffset:CGPointMake(0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated:YES];
-
Gank over 9 yearsThis did solve my problem. But now I'm using
SVPullToRefresh
, how to pull it down programmatically? -
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 about 9 yearshi,Thank you a lot,I am also curious about the magic point (0, -64) I met when debugging.
-
Igotit about 9 years@inix 20 for status bar height + 44 for navigation bar height
-
Diogo T over 8 yearsInstead of
-64
is better to use-self.topLayoutGuide.length
-
Koen. over 8 yearsDocumentation 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 about 8 yearsWorks for UITableView.
-
Colin Basnett almost 8 yearsI would recommend putting
sendActionsForControlEvents(UIControlEvents.ValueChanged)
at the end of this function, otherwise the actual refresh logic logic will not be run. -
JoeGalind over 7 yearsThis 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 over 7 yearsPerfect! 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 over 7 yearsI've merged this with the accepted answer as it's just an update.
-
Jadamec over 7 yearsI had to call beginRefreshing() in viewDidAppear instead of viewDidLoad, otherwise only the title was visible, not the spinning animation.
-
Bilal over 7 yearsThis should fix the problem - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ [refreshControl beginRefreshing]; }); }
-
carbonr about 7 yearsIts the
sendActions
to triggerrx
that makes this answer related toRxSwift
incase anyone is wondering at first look -
nmdias almost 7 yearsI 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 almost 7 yearsJust a general opinion... This needs to be radar'ed if it hasn't been fixed in 11. This seems like a oversight.
-
Bassebus over 6 yearsOf all the answers above, this was the only one I could get working on iOS 11.1 / xcode 9.1
-
SinisterMJ almost 6 years@ColinBasnett : Adding that code (which is now sendActions(for: UIControlEvents.valueChanged)), results in an infinite loop...
-
Marc Etcheverry over 5 yearsI have noted issues with using the setContentOffset:animated method, so this solution worked for me.
-
Morten Holmgaard about 5 yearsChange the beginRefreshing() to the last line to fix problem with tintColor not respected on first show: stackoverflow.com/a/20383030/860488
-
francybiga over 4 yearsThe
asyncAfter
is actually what makes the spinner animation work (iOS 12.3 / Xcode 10.2) -
jegadeesh about 3 yearscalling send actions method is the only way it's working for me!
-
Mishka almost 3 yearstriggers infinity loop