308
votes

Just curious, as it doesn't immediately seem possible, but is there a sneaky way to leverage the new iOS 6 UIRefreshControl class without using a UITableViewController subclass?

I often use a UIViewController with a UITableView subview and conform to UITableViewDataSource and UITableViewDelegate rather than using a UITableViewController outright.

12
@DaveDeLong : No, Dave, what should he do? Later on this page you say his solution isn't supported, so what's the right solution?matt
@matt he should use a UITableViewController and file a bug requesting API to use a UIRefreshControl with a UITableView directly.Dave DeLong
UITableViewController has had (and continues to have) too many obscure and niche bugs and problems with non-trivial view hierarchies ... that all magically disappear when you switch to using a standard VC with a TableView subview. "Use UITVC" is a poor start to any solution, IMHO.Adam
@Adam Do these bugs surface when using the 'UITableViewController' as a child view controller (thus giving access to view customization without mucking around in the tableViewControllers hierarchy)? I've never encountered issues when using it this way.memmons

12 Answers

388
votes

On a hunch, and based on DrummerB's inspiration, I tried simply adding a UIRefreshControl instance as a subview to my UITableView. And it magically just works!

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

This adds a UIRefreshControl above your table view and works as expected without having to use a UITableViewController :)


EDIT: This above still works but as a few have pointed out, there is a slight "stutter" when adding the UIRefreshControl in this manner. A solution to that is to instantiate a UITableViewController, and then setting your UIRefreshControl and UITableView to that, i.e.:

UITableViewController *tableViewController = [[UITableViewController alloc] init];
tableViewController.tableView = self.myTableView;

self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:@selector(getConnections) forControlEvents:UIControlEventValueChanged];
tableViewController.refreshControl = self.refreshControl;
94
votes

To eliminate the stutter that is caused by the accepted answer, you can assign your UITableView to a UITableViewController.

_tableViewController = [[UITableViewController alloc]initWithStyle:UITableViewStylePlain];
[self addChildViewController:_tableViewController];

_tableViewController.refreshControl = [UIRefreshControl new];
[_tableViewController.refreshControl addTarget:self action:@selector(loadStream) forControlEvents:UIControlEventValueChanged];

_theTableView = _tableViewController.tableView;

EDIT:

A way to add a UIRefreshControl with no UITableViewController with no stutter and retain the nice animation after refreshing data on the tableview.

UIRefreshControl *refreshControl = [UIRefreshControl new];
[refreshControl addTarget:self action:@selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
[self.theTableView addSubview:refreshControl];
[self.theTableView sendSubviewToBack:refreshControl];

Later when handling the refreshed data...

- (void)handleRefresh:(UIRefreshControl *)refreshControl {
    [self.theTableView reloadData];
    [self.theTableView layoutIfNeeded];
    [refreshControl endRefreshing];
}
21
votes

What you would try is use container view inside ViewController you are using. you can define clean UITableViewController subclass with dedicated tableview and place that in the ViewController.

18
votes

Well UIRefreshControl is a UIView subclass, so you can use it on it's own. I'm not sure though how it renders itself. The rendering could simply depend on the frame, but it also could rely on a UIScrollView or the UITableViewController.

Either way, it's going to be more of a hack than an elegant solution. I recommend you look into one of the available 3rd party clones or write your own.

ODRefreshControl

enter image description here

SlimeRefresh

enter image description here

7
votes

Try delaying the call to the refreshControl -endRefresh method by a fraction of a second after the tableView reloads its contents, either by using NSObject's -performSelector:withObject:afterDelay: or GCD's dispatch_after.

I created a category on UIRefreshControl for this:

@implementation UIRefreshControl (Delay)

- (void)endRefreshingAfterDelay:(NSTimeInterval)delay {
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self endRefreshing];
    });
}

@end

I tested it and this also works on Collection Views. I've noticed that a delay as small as 0.01 seconds is enough:

// My data refresh process here while the refresh control 'isRefreshing'
[self.tableView reloadData];
[self.refreshControl endRefreshingAfterDelay:.01];
7
votes

IOS 10 Swift 3.0

it's simple

import UIKit

class ViewControllerA: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet weak var myTableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        myTableView.delegate = self
        myTableView.dataSource = self

        if #available(iOS 10.0, *) {
            let refreshControl = UIRefreshControl()
            let title = NSLocalizedString("PullToRefresh", comment: "Pull to refresh")
            refreshControl.attributedTitle = NSAttributedString(string: title)
            refreshControl.addTarget(self,
                                     action: #selector(refreshOptions(sender:)),
                                     for: .valueChanged)
            myTableView.refreshControl = refreshControl
        }
    }

    @objc private func refreshOptions(sender: UIRefreshControl) {
        // Perform actions to refresh the content
        // ...
        // and then dismiss the control
        sender.endRefreshing()
    }

    // MARK: - Table view data source

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 12
    }


    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)

        cell.textLabel?.text = "Cell \(String(indexPath.row))"
        return cell
    }

}

enter image description here

If you want to learn about iOS 10 UIRefreshControl read here.

3
votes

Adding the refresh control as a subview creates an empty space above section headers.

Instead, I embedded a UITableViewController into my UIViewController, then changed my tableView property to point towards the embedded one, and viola! Minimal code changes. :-)

Steps:

  1. Create a new UITableViewController in Storyboard and embed it into your original UIViewController
  2. Replace @IBOutlet weak var tableView: UITableView! with the one from the newly embedded UITableViewController, as shown below

class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let tableViewController = self.childViewControllers.first as! UITableViewController
        tableView = tableViewController.tableView
        tableView.dataSource = self
        tableView.delegate = self

        // Now we can (properly) add the refresh control
        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action: "handleRefresh:", forControlEvents: .ValueChanged)
        tableViewController.refreshControl = refreshControl
    }

    ...
}
2
votes

For Swift 2.2 .

First make UIRefreshControl() .

var refreshControl : UIRefreshControl!

In your viewDidLoad() method add:

refreshControl = UIRefreshControl()
    refreshControl.attributedTitle = NSAttributedString(string: "Refreshing..")
    refreshControl.addTarget(self, action: #selector(YourUIViewController.refresh(_:)), forControlEvents: UIControlEvents.ValueChanged)
    self.tableView.addSubview(refreshControl)

And make refresh function

func refresh(refreshControl: UIRefreshControl) {

    // do something ...

    // reload tableView
    self.tableView.reloadData()

    // End refreshing
    refreshControl.endRefreshing()
}
0
votes

Here's another solution which is a little different.

I had to use it because of some view hierarchy issues I had: I was creating some functionality that required passing views around to different places in the view hierarchy, which broken when using a UITableViewController's tableview b/c the tableView is the UITableViewController's root view (self.view) and not just a regular view, it created inconsistent controller / view hierarchies and caused a crash.

Basically create your own subclass of UITableViewController and override loadView to assign self.view a different view, and override the tableView property to return a separate tableview.

for example:

@interface MyTableVC : UITableViewController
@end

@interface MyTableVC ()
@property (nonatomic, strong) UITableView *separateTableView;
@end

@implementation MyTableVC

- (void)loadView {
    self.view = [[UIView alloc] initWithFrame:CGRectZero];
}

- (UITableView *)tableView {
    return self.separateTableView;
}

- (void)setTableView:(UITableView *)tableView {
    self.separateTableView = tableView;
}

@end

When combined with Keller's solution this will more robust in the sense that the tableView is now a regular view, not a VC's root view, and be more robust against changing view hierarchies. Example of using it this way:

MyTableVC *tableViewController = [[MyTableVC alloc] init];
tableViewController.tableView = self.myTableView;

self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:@selector(getConnections) forControlEvents:UIControlEventValueChanged];
tableViewController.refreshControl = self.refreshControl;

There is another possible use for this:

Since subclassing this way separates self.view from self.tableView, it's possible now to use this UITableViewController as more of a regular controller, and add other subviews to self.view without the oddities of adding subviews to UITableView, so one may considering making their view controllers directly a subclass of UITableViewController instead of having UITableViewController children.

Some things to watch out for:

Since we're overriding the tableView property without calling super, there may be some things to watch out for and should handle where necessary. For example, setting the tableview in my above example will not add the tableview to self.view and not set the frame which you may want to do. Also, in this implementation there is no default tableView given to you when the class is instantiated, which is also something you may consider adding. I don't include it here because that is case by case, and this solution actually fits well with Keller's solution.

0
votes

Try this,

The Above solutions are fine but tableView.refreshControl is available for UITableViewController only, till iOS 9.x and comes in UITableView from iOS 10.x onwards.

Written in Swift 3 -

let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(FeedViewController.loadNewData), for: UIControlEvents.valueChanged)
// Fix for the RefreshControl Not appearing in background
tableView?.addSubview(refreshControl)
tableView.sendSubview(toBack: refreshControl)
-1
votes

Keller's first suggestion causes a strange bug in iOS 7 where the inset of the table is increased after the view controller reappears. Changing to the second answer, using the uitableviewcontroller, fixed things for me.

-6
votes

It turns out you can use the following when you use a UIViewController with a UITableView subview and conform to UITableViewDataSource and UITableViewDelegate:

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