UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController
Asked Answered
A

15

133

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

Architecture answered 5/2, 2013 at 23:23 Comment(0)
D
209

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)        
    }
}
Depositor answered 6/2, 2013 at 0:43 Comment(10)
Thanks - that achieved the effect I was after! I had to set the content offset back to 0, 0 once refreshing was finished also.Architecture
That's strange, in my tests, endRefreshing adjusts offset as neededDepositor
It did sometimes, and not others. Could be related to the UINavigationController issue? Not sure :SArchitecture
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];Dinkins
@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.Pasteurizer
@EricBaker you can use: [self.tableView setContentOffset:CGPointMake(0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated:YES];Blum
I had to call beginRefreshing() in viewDidAppear instead of viewDidLoad, otherwise only the title was visible, not the spinning animation.Hawkbill
This should fix the problem - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ [refreshControl beginRefreshing]; }); }Tesch
Just a general opinion... This needs to be radar'ed if it hasn't been fixed in 11. This seems like a oversight.Entero
Change the beginRefreshing() to the last line to fix problem with tintColor not respected on first show: https://mcmap.net/q/169006/-ios-7-uirefreshcontrol-tintcolor-not-working-for-beginrefreshingExtraterritoriality
D
82

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];
Dentate answered 18/3, 2014 at 5:34 Comment(3)
hi,Thank you a lot,I am also curious about the magic point (0, -64) I met when debugging.Ahem
@Ahem 20 for status bar height + 44 for navigation bar heightDentate
Instead of -64 is better to use -self.topLayoutGuide.lengthCultivated
S
36

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()
    }
}
Sinistrorse answered 17/2, 2016 at 21:50 Comment(4)
Works for UITableView.Montgolfier
I would recommend putting sendActionsForControlEvents(UIControlEvents.ValueChanged) at the end of this function, otherwise the actual refresh logic logic will not be run.Strasbourg
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!Urgent
@ColinBasnett : Adding that code (which is now sendActions(for: UIControlEvents.valueChanged)), results in an infinite loop...Stidham
H
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.

Helico answered 20/5, 2014 at 18:1 Comment(3)
This did solve my problem. But now I'm using SVPullToRefresh, how to pull it down programmatically?Ultun
@Ultun 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/SVPullToRefreshHelico
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)Wetterhorn
S
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];
Subteen answered 13/2, 2015 at 12:50 Comment(0)
C
12

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!

Coulee answered 27/5, 2018 at 10:53 Comment(0)
D
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){

        }];

    }
}
Delia answered 27/4, 2013 at 10:6 Comment(3)
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 :/Soluk
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.Irresolute
I have noted issues with using the setContentOffset:animated method, so this solution worked for me.Janeejaneen
B
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()
      }
   }
}
Biracial answered 10/10, 2017 at 12:46 Comment(2)
Of all the answers above, this was the only one I could get working on iOS 11.1 / xcode 9.1Alehouse
The asyncAfter is actually what makes the spinner animation work (iOS 12.3 / Xcode 10.2)Aqaba
J
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)
Jolly answered 16/2, 2017 at 15:30 Comment(0)
D
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)
    }

}
Dirac answered 14/4, 2019 at 10:46 Comment(2)
calling send actions method is the only way it's working for me!Chimere
triggers infinity loopCremator
D
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.

Dowsabel answered 11/3, 2014 at 12:21 Comment(0)
D
4

Fort Swift 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
Daughtry answered 25/5, 2016 at 18:28 Comment(1)
I've merged this with the accepted answer as it's just an update.Bairam
P
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.

Pianette answered 5/5, 2017 at 6:27 Comment(2)
Its the sendActions to trigger rx that makes this answer related to RxSwift incase anyone is wondering at first lookMarquittamarr
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)Besides
D
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() 
    }
}
Donothing answered 26/3, 2020 at 16:53 Comment(0)
P
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

Ph answered 23/12, 2016 at 21:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.