26

I'm using the following code to create a UIRefreshControl:

- (void) viewDidLoad
{
    [super viewDidLoad];

    UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
    [refreshControl addTarget:self action:@selector(doLoad) forControlEvents:UIControlEventValueChanged];
    self.refreshControl = refreshControl;
}

- (void) doLoad
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // Instead of sleeping, I do a webrequest here.
            [NSThread sleepForTimeInterval: 5];

            dispatch_async(dispatch_get_main_queue(), ^{
                [tableView reloadData];
                [self.refreshControl endRefreshing];
            });
    });
}

It works great. If I navigate to my view, drag the table, the code runs and the data displays.

However, what I would like to do is have the view in the 'loading' state as soon as it appears (that way the user knows something is going on). I have tried adding the following:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self.refreshControl beginRefreshing];
}

But it does not seem to work. When I navigate to the view, it looks like a regular view (refresh control is not visible), plus when I try to pull the refresh control, it never finished loading.

Obviously I'm going about this the wrong way. Any suggestions on how I should handle this?

Kyle
  • 17,317
  • 32
  • 140
  • 246
  • It works for me. I do the exact same thing. Only that the action for the refreshControl has a sender property. When I enter the screen the refresh control isn't visible but if I pull the table view a bit down I can see the spinning refresh control. – dasdom Aug 01 '13 at 17:10

4 Answers4

68

Try this:

- (void) viewWillAppear: (BOOL) animated
{
    [super viewWillAppear: animated];

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

    // kick off your async refresh!
    [self doLoad];
}

Remember to call endRefreshing at some point!

EDIT to add full working sample:

This sample view controller, built and run in iOS6.1 as the root viewcontroller starts with the UIRefreshControl already visible and animating when the app launches.

TSTableViewController.h

@interface TSTableViewController : UITableViewController
@end

TSTableViewController.m

#import "TSTableViewController.h"

@implementation TSTableViewController
{
    NSMutableArray*    _dataItems;
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    self.refreshControl = [UIRefreshControl new];

    [self.refreshControl addTarget: self
                            action: @selector( onRefresh: )
                  forControlEvents: UIControlEventValueChanged];
}

- (void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear: animated];

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

    [self.refreshControl beginRefreshing];
    [self onRefresh: nil];
}

- (void) onRefresh: (id) sender
{
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

        _dataItems = [NSMutableArray new];

        for ( int i = 0 ; i < arc4random() % 100 ; i++ )
        {
            CFUUIDRef uuid = CFUUIDCreate( NULL );

            [_dataItems addObject: CFBridgingRelease(CFUUIDCreateString( NULL, uuid)) ];

            CFRelease( uuid );
        }

        [self.refreshControl endRefreshing];

        [self.tableView reloadData];
    });
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView:(UITableView *) tableView numberOfRowsInSection: (NSInteger) section
{
    return _dataItems.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
                                                   reuseIdentifier: nil];

    cell.textLabel.text = [_dataItems objectAtIndex: indexPath.row];

    return cell;
}

@end
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • 1
    Is this working for you? I've been trying but it still does not show anything until I swipe my finger on the table. – Kyle Aug 02 '13 at 19:00
  • 2
    Works only if the contentOffset is set before call of beginRefreshing – ıɾuǝʞ Mar 10 '14 at 13:31
  • Adjusting contentOffset does make the control visible, but for me, it won't spin, even if I call beginRefreshing (before or after changing the content offset). – Mark Krenek Oct 20 '15 at 22:20
15

Manually modifying the contentOffset is insecure and wrong and can lead to unexpected behavior in some cases. This solution works without touching the contentOffset at all:

func showRefreshControl(show: Bool) {
    if show {
        refreshControl?.beginRefreshing()
        tableView.scrollRectToVisible(CGRectMake(0, 0, 1, 1), animated: true)
    } else {
        refreshControl?.endRefreshing()
    }
}
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
  • 1
    This is not correct. It is perfectly valid (and often necessary) to modify the content offset (for example, to offset the content by the length of the top layout guide). – Greg Brown Aug 24 '15 at 19:01
  • 2
    "This is not correct. It's perfectly valid". Really no argument there, that's just your opinion. You assume that you're the only one touching the `contentOffset`, while you're not. The problem is that you have to **reset** the content offset when refreshing - and you can't possibly know what the correct value is for the reset if other components of `UIKit` may also modify the offset at the same time. –  Aug 25 '15 at 08:30
  • Saying it is "insecure and wrong" is an opinion. Saying that it is valid to modify the content offset is not - in fact, Apple's scroll view programming guide specifically mentions setting the content offset. For example, in order to take layout guides into account, your application may need to manually set the offset. Doing this in viewDidLayoutSubviews ensures that your code gets the last say in what the content offset should be. – Greg Brown Aug 26 '15 at 12:57
  • 1
    i like this technique as it seems cleaner than modifying offsets. i'm finding though that since my table is empty, the initial scroll gets ignored. using `tableView.scrollRectToVisible(CGRectMake(0, -1, 1, 1), animated: false)` works though. also, if during that first animation i pull it again, the refreshcontrol height drops 20 points for no apparent reason when it reaches 119 and then continues as if nothing had happened. – joe May 23 '16 at 20:01
3

Another option is fire a UIControlEventValueChanged in your viewDidAppear: to trigger an initial refresh.

dklt
  • 1,703
  • 1
  • 12
  • 12
0
   - (void) viewDidAppear: (BOOL) animated
   {
          [super viewDidAppear: animated];
          UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
          [refreshControl addTarget:self action:@selector(doLoad) forControlEvents:UIControlEventValueChanged];
          self.refreshControl = refreshControl;
          [self.refreshControl beginRefreshing];    

   }

You never set self.refreshControl

Abdullah Shafique
  • 6,878
  • 8
  • 35
  • 70