UIRefreshControl without UITableViewController
Asked Answered
K

12

307

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.

Kao answered 19/9, 2012 at 15:30 Comment(5)
@DaveDeLong : No, Dave, what should he do? Later on this page you say his solution isn't supported, so what's the right solution?Defeatism
@Defeatism he should use a UITableViewController and file a bug requesting API to use a UIRefreshControl with a UITableView directly.Canotas
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.Bullroarer
@Bullroarer 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.Anatollo
Possible duplicate of Pull to refresh UITableView without UITableViewControllerConlee
K
388

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;
Kao answered 19/9, 2012 at 20:32 Comment(18)
FYI, this is unsupported behavior and may change in the future. The only guarantee that Apple makes is that using it according to the provided api (in this case -[UITableViewController setRefreshControl:]) will continue to function.Canotas
With this implementation, it seems that right before triggering handleRefresh: there's an unexpected change in value of the scroll view's contentInsets for a split second. Anyone else experience this or have a fix for it? (yup, I know this is unsupported in the first place!)Panne
You really should simply use a Container View for this. Just set its view controller's class to your custom subclass of UITableViewController and you'll be "doing it right".Leaseholder
Added an answer which doesn't have the stutter problem.Kcal
In iOS7 (unknown for iOS6) the edited version works fine except for when you close and reopen the app. The animation doesn't play until the 2nd time you refresh. The first time refreshing after reopening the app, its just a full circle instead of the incrementing circle based on how far you pull it down. Is there a fix?Graiggrail
It's been a while since this answer was posted and accepted. How has this solution been working for everyone so far?Contestant
I still get a slight 'stutter' in my tableview using the 'edit' solution in this answer. It's not as bad as when using addSubView but it is still quite noticeable.Interbreed
this answer is a much more elegant solution, and you can still use the interface builder and you arent doing anything dodgy in terms of unsupported behaviourCote
To avoid having the "slutter" as mentioned above while still bypassing the UITableViewController, just add the refresh control beneath the table view like so: [_tableView insertSubview:_refreshControl atIndex:0];. Tested with both iOS 7 and 8 ;)Expense
@ptitvinou can you elaborate more? I may be stupid with these controls but I can't get it to work without the 'jump' or 'slutter', it just doesn't look good anyhowSculpsit
@ptitvinou thanks for the quick reply! Apparently my issue is a different one than this "slutter". When I add the UIRefreshControl to the uitableview, it 'jumps' when it hits the 'refresh' offset. When I add it to UITableViewController it jumps too. Did you meet this issue too? I couldn't find any working solution for me (for iOS 7,8) in any of the uirefreshcontrol x uitableview threads...Sculpsit
As for the jump issue I do have a little one when a reach the refresh offset. I must admit I don't know why and not really a concern to me but If you already tried with a UITableViewController (which I didn't) and this happens too then I'd say that this is the component limitation. Don't hesitate to share a solution if you find one :)Expense
I ended up using a third-party class SVPullToRefresh (github.com/samvermette/SVPullToRefresh) which had it's issues too (= with trying to hide search bar on UITableView with contentSize lower than tableView height....) but with no text the arrow itself makes a nice effect, and at least it doesn't have 'unsolvable internal' problems like the default UIRefreshControl :)Sculpsit
I've solved problem with "stutter" by removing tableView.estimatedRowHeightDolora
The basic version works for me except that the refresh control is aligned right of center. No idea how to fix that either.Kinelski
@alper are you sure your table view has appropriate auto layout constraints connected to your view (leading and trailing in particular)?Kao
Ah, that was indeed it. My entire table view was stretching right off screen and it took me some time to figure that out (the UI inspector doesn't really show that).Kinelski
Just had a little problem and this might be helpful. Don't forget setting tableView's bounces property to YES ;)Orthognathous
K
94

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];
}
Kcal answered 3/1, 2013 at 22:14 Comment(8)
You don't even have to create a new UITableView. Just say initWithStyle:existingTableView.style - and then afterwards do newTableViewController.tableView = existingTableView and assign refreshControl. Boils this down to three lines.Apgar
Don't forget to call [_tablewViewController didMoveToParentViewController:self]; after adding the subview.Wadesworth
self.tableView = _tableViewController.tableView; should be _tableViewController.tableView = self.tableViewTbar
@Linus why is that neccessary? I added _tableViewController.view as a subView in my custom viewController's viewDidLoad method and that seemed to do the trick. I didn't even need to set _tableViewController.tableViewCollapse
The reason that I cannot user a UITableViewController and am using a UIViewController w/ a table view inside. #18900928Lanfri
@Lanfri you can absolutely use what I have here within a UIViewController the example above is actually inside a UIViewController ViewDidLoad function.Kcal
This solution is nearly there - however for me, at the point where the refresh is triggered, the tableView jumps about 40 pixels down the screen.Arnold
Still seeing the stutter with iOS 9 ... anyone else have it working ?Kinkajou
A
21

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.

Adolphus answered 25/10, 2012 at 21:9 Comment(3)
Precisely, the cleanest way seems to be adding child view controller of UITableViewController type.Todhunter
You can then grab the UITableViewController using self.childViewControllers.firstObject.Naivete
^ you might also want to consider using prepareForSegue to get a reference to the UITableViewController object in a container view as this is immediately triggered as a segue event on instantiation from the storyboard.Robles
P
18

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

Paphlagonia answered 19/9, 2012 at 15:38 Comment(6)
Thanks for your response, but not exactly what I was looking for. The first sentence of your post did, however, inspire me to try what ended up being the solution though!Kao
ODRefreshControl is awesome - thanks for the tip! Very simple, and does the job. And of course much more flexible than Apple's. I never understood why anyone would use UITableViewController, IMO the worst class in the whole API. It does (next to) nothing, but makes your views inflexible.Overdo
It actually depends on your project what you use in there .. using a uitableview in my case was increasing complexity of the project and now i switched to uitableviewcontroller which sort of divided my code into two segments i am happy i have 2 smaller files that can be used to debug instead of one huge file ..Vanmeter
@Overdo Hello, a good reason to use UITableViewController is being able to design with static cells. Unfortunately unavailable in a tableView residing in a UIViewController.Sabaean
@JeanLeMoignanThe iOS tools and libraries are changing quickly so a 3 year old comment of mine is no longer valid. I've switched to creating all user interfaces in code with SnapKit and honestly it's hugely more efficient than clicking 10 thousand times in interface builder. Also changing a UITableViewController to a UIViewController is easy and only takes a second, whereas in IB it's a big pain.Overdo
Oh and refreshcontrol is no longer an issue... no need for 3rd party libraries anymore.Overdo
H
7

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];
Herring answered 3/8, 2013 at 4:48 Comment(4)
Delays and waits are really bad style.Dollarfish
@Dollarfish I agree. This is a workaround for an already unsupported use of UIRefreshControl.Herring
You can call a method with a delay a lot easier with. [self performSelector:@selector(endRefreshing) withObject:nil afterDelay:0.01]Dollarfish
The end effect is exactly the same, and doesn’t makes the ‘hack’ more/less elegant. Whether to use -performSelector:withObject:afterDelay: or GCD for this is up to a matter of personal taste.Herring
S
7

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.

Summerwood answered 22/12, 2016 at 15:49 Comment(0)
N
3

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
    }

    ...
}
Naivete answered 5/3, 2015 at 5:46 Comment(0)
I
2

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()
}
Internationale answered 17/5, 2016 at 20:6 Comment(0)
C
0

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.

Cornfield answered 8/1, 2014 at 21:36 Comment(1)
The question specified "without using a UITableViewController subclass", and this answer is for a UITableViewController subclass.Outman
H
0

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)
Huai answered 12/12, 2016 at 5:12 Comment(1)
Best solution so farDisperse
V
-1

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.

Venessavenetia answered 25/9, 2013 at 11:13 Comment(0)
D
-6

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];
Domel answered 14/9, 2013 at 17:25 Comment(2)
Disagree. self.refreshControl is a property for a UITableViewController, not a UIViewController.Metalinguistics
NOT WORKING, refershControl is not defined for uiviewcontrollerVerminous

© 2022 - 2024 — McMap. All rights reserved.