Reputation: 11147
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
Upvotes: 133
Views: 74394
Reputation: 4064
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()
}
}
}
Upvotes: 7
Reputation: 1210
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()
}
}
Upvotes: 3
Reputation: 417
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)
}
}
Upvotes: 5
Reputation: 32434
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)
}
}
Upvotes: 209
Reputation: 1648
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!
Upvotes: 12
Reputation: 592
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)
Upvotes: 5
Reputation: 168
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.
Upvotes: 4
Reputation: 2005
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];
}
}
Upvotes: 1
Reputation: 2634
Fort Swift 2.2+
self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
Upvotes: 4
Reputation: 21013
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()
}
}
Upvotes: 37
Reputation: 4884
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];
Upvotes: 13
Reputation: 3060
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.
Upvotes: 15
Reputation: 1076
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];
Upvotes: 82
Reputation: 1306
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.
Upvotes: 4
Reputation: 21005
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){
}];
}
}
Upvotes: 8