83

I have about 5 UIScrollView's already in my app which all load multiple .xib files. We now want to use a UIRefreshControl. They are built to be used with UITableViewControllers (per UIRefreshControl class reference). I do not want to re-do how all 5 UIScrollView work. I have already tried to use the UIRefreshControl in my UIScrollView's, and it works as expected except for a few things.

  1. Just after the refresh image turns into the loader, the UIScrollView jumps down about 10 pixels, which only does not happen when I am very careful to drag the UIScrollview down very slowly.

  2. When I scroll down and initiate the reload, then let go of the UIScrollView, the UIScrollView stays where I let it go. After it is finished reloading, the UIScrollView jumps up to the top with no animation.

Here is my code:

-(void)viewDidLoad
{
      UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
      [refreshControl addTarget:self action:@selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
      [myScrollView addSubview:refreshControl];
}

-(void)handleRefresh:(UIRefreshControl *)refresh {
      // Reload my data
      [refresh endRefreshing];
}

Is there any way I can save a bunch of time and use a UIRefreshControl in a UIScrollView?

Thank You!!!

Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
SirRupertIII
  • 12,324
  • 20
  • 72
  • 121
  • 1
    +1 for discovering that you can even do this. But, I'm having trouble reproducing either symptom. I notice a completely different problem that the `attributedTitle` appears in the wrong place the first time I pull down to refresh unless I explicitly set the initial `attributedTitle` (e.g. to something like "Pull to refresh") when I first create the refresh control. So, a couple of questions: 1. Are you initializing the `attributedTitle` to anything during this initial creation? Also, 2. Are you using auto layout? 3. Are you doing anything else with any `UIScrollViewDelegate` methods? – Rob Feb 16 '13 at 03:45

11 Answers11

97

I got a UIRefreshControl to work with a UIScrollView:

- (void)viewDidLoad
{
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)];
    scrollView.userInteractionEnabled = TRUE;
    scrollView.scrollEnabled = TRUE;
    scrollView.backgroundColor = [UIColor whiteColor];
    scrollView.contentSize = CGSizeMake(500, 1000);

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

    [self.view addSubview:scrollView];
}

- (void)testRefresh:(UIRefreshControl *)refreshControl
{    
    refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Refreshing data..."];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        [NSThread sleepForTimeInterval:3];//for 3 seconds, prevent scrollview from bouncing back down (which would cover up the refresh view immediately and stop the user from even seeing the refresh text / animation)

        dispatch_async(dispatch_get_main_queue(), ^{
            NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
            [formatter setDateFormat:@"MMM d, h:mm a"];
            NSString *lastUpdate = [NSString stringWithFormat:@"Last updated on %@", [formatter stringFromDate:[NSDate date]]];

            refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:lastUpdate];

            [refreshControl endRefreshing];

            NSLog(@"refresh end");
        });
    });
}

Need to do the data update on a separate thread or it will lock up the main thread (which the UI uses to update the UI). So while the main thread is busy updating the data, the UI is also locked up or frozen and you never see the smooth animations or spinner.

EDIT: ok, I'm doing the same thing as OP and i've now added some text to it (ie, "Pull to Refresh") and it does need to get back onto the main thread to update that text.

Updated answer.

Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
Padin215
  • 7,444
  • 13
  • 65
  • 103
  • 4
    You'll probably want to put your `endRefreshing` in a main queue block though, since it's a UI method. – Scott Berrevoets Feb 21 '13 at 19:20
  • I thought so as well and my first post included retrieving the main thread, but this doesn't cause any errors or crashes. – Padin215 Feb 21 '13 at 20:11
  • This solution is working for me. However, the uirefreshcontrol spinner is on top of everything in my view. Is there any way to get to to show up below my view? – Salx Sep 16 '14 at 20:50
  • @Salx ... same issue :/ – jsetting32 Oct 29 '14 at 21:58
  • 2
    @jsetting32 - I fixed my problem with this line of code: [refreshControl.superview sendSubviewToBack:refreshControl]; – Salx Oct 31 '14 at 19:59
  • This does not answer the original question… – kubbing Jan 05 '15 at 15:32
  • Is there any way how to use the UIRefreshControl at the bottom of the scroll view? I have added one but it does not appear on that place. The top one is ok, but at the bottom it does not seem to work with appropriate constraints. – Vladimír Slavík Feb 12 '15 at 22:00
  • From Apple's documentation - **Because the refresh control is specifically designed for use in a table view that's managed by a table view controller, using it in a different context can result in undefined behavior.** – Benson Jul 26 '16 at 01:00
  • This does not work for iOS 10. For iOS 10, refer to Ben Packard's answer. – Siyu Apr 16 '17 at 22:01
  • Is there a reason why you sleep for 3 seconds or is that just for show? – Albert Renshaw Oct 29 '17 at 00:53
  • @AlbertRenshaw no specific reason. – Padin215 Nov 03 '17 at 22:25
46

Adding to above answers, in some situations you can't set the contentSize (using auto layout perhaps?) or the contentSize's height is less than or equal the height of the UIScrollView. In these cases, the UIRefreshControl won't work because the UIScrollView won't bounce.

To fix this set the property alwaysBounceVertical to TRUE.

Diego Frata
  • 1,028
  • 7
  • 15
31

Since iOS 10 UIScrollView already has a refreshControl property. This refreshControl will appear when you create a UIRefereshControl and assign it to this property.

There's no need to add UIRefereshControl as a subview anymore.

func configureRefreshControl () {
   // Add the refresh control to your UIScrollView object.
   myScrollingView.refreshControl = UIRefreshControl()
   myScrollingView.refreshControl?.addTarget(self, action:
                                      #selector(handleRefreshControl),
                                      for: .valueChanged)
}

@objc func handleRefreshControl() {
   // Update your content…

   // Dismiss the refresh control.
   DispatchQueue.main.async {
      self.myScrollingView.refreshControl?.endRefreshing()
   }
}

A UIRefreshControl object is a standard control that you attach to any UIScrollView object

Code and quote from https://developer.apple.com/documentation/uikit/uirefreshcontrol

SirRupertIII
  • 12,324
  • 20
  • 72
  • 121
MacMark
  • 6,239
  • 2
  • 36
  • 41
14

If and when you are fortunate enough to be supporting iOS 10+, you can now simply set the refreshControl of the UIScrollView. This works the same way as the previously existing refreshControl on UITableView.

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
11

Here is how you do this in C# / Monotouch. I cant find any samples for C# anywhere, so here it is.. Thanks Log139!

public override void ViewDidLoad ()
{
    //Create a scrollview object
    UIScrollView MainScrollView = new UIScrollView(new RectangleF (0, 0, 500, 600)); 

    //set the content size bigger so that it will bounce
    MainScrollView.ContentSize = new SizeF(500,650);

    // initialise and set the refresh class variable 
    refresh = new UIRefreshControl();
    refresh.AddTarget(RefreshEventHandler,UIControlEvent.ValueChanged);
    MainScrollView.AddSubview (refresh);
}

private void RefreshEventHandler (object obj, EventArgs args)
{
    System.Threading.ThreadPool.QueueUserWorkItem ((callback) => {  
        InvokeOnMainThread (delegate() {
        System.Threading.Thread.Sleep (3000);             
                refresh.EndRefreshing ();
        });
    });
}
Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
John Gavilan
  • 111
  • 2
2

For the Jumping issue, Tim Norman's answer solves it.

Here is the swift version if you are using swift2:

import UIKit

class NoJumpRefreshScrollView: UIScrollView {

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
    // Drawing code
}
*/
override var contentInset:UIEdgeInsets {
    willSet {
        if self.tracking {
            let diff = newValue.top - self.contentInset.top;
            var translation = self.panGestureRecognizer.translationInView(self)
            translation.y -= diff * 3.0 / 2.0
            self.panGestureRecognizer.setTranslation(translation, inView: self)
            }
        }
    }
}
Community
  • 1
  • 1
Surely
  • 1,649
  • 16
  • 23
2

How to do it in Swift 3:

override func viewDidLoad() {
    super.viewDidLoad()

    let scroll = UIScrollView()
    scroll.isScrollEnabled = true
    view.addSubview(scroll)

    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action: #selector(pullToRefresh(_:)), for: .valueChanged)
    scroll.addSubview(refreshControl)
}

func pullToRefresh(_ refreshControl: UIRefreshControl) {
    // Update your conntent here

    refreshControl.endRefreshing()
}
Camilo Ortegón
  • 3,414
  • 3
  • 26
  • 34
1

I made UIRefreshControl work properly inside UIScrollView. I inherited UIScrollView, blocked changing of contentInset and overrided contentOffset setter:

class ScrollViewForRefreshControl : UIScrollView {
    override var contentOffset : CGPoint {
        get {return super.contentOffset }
        set {
            if newValue.y < -_contentInset.top || _contentInset.top == 0 {
                super.contentOffset = newValue
            }
        }
    }
    private var _contentInset = UIEdgeInsetsZero
    override var contentInset : UIEdgeInsets {
        get { return _contentInset}
        set {
            _contentInset = newValue
            if newValue.top == 0 && contentOffset.y < 0 {
                self.setContentOffset(CGPointZero, animated: true)
            }
        }
    }
}
Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
Anton Zherdev
  • 674
  • 1
  • 8
  • 10
  • 1
    This solution seemed promising at first, but it had the (unexplained?) side-effect that my scroll view didn't respond to user interaction for a few seconds after loading the view. – cthulhu Mar 30 '15 at 12:05
1

For the Jumping issue, override contentInset only solves it before iOS 9. I just tried a way to avoid jump issue:

let scrollView = UIScrollView()
let refresh = UIRefreshControl()
//        scrollView.refreshControl = UIRefreshControl() this will cause the jump issue
refresh.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged)
scrollView.alwaysBounceVertical = true
//just add refreshControl and send it to back will avoid jump issue
scrollView.addSubview(refresh)
scrollView.sendSubviewToBack(refresh)

works on iOS 9 10 11 and so on,and I hope they(Apple) just fix the issue.

Dante
  • 81
  • 1
  • 2
0

Starting with iOS 10 a UIScrollView has a refreshControl property that you can set to a UIRefreshControl. As always you do not need to manage the frame of the control. Just configure the control, set the target-action for the valueChanged event and assign to the property.

Regardless of whether you are using a plain scroll view, table view or collection view the steps to create a refresh control are the same.

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

@objc private func refreshOptions(sender: UIRefreshControl) {
  // Perform actions to refresh the content
  // ...
  // and then dismiss the control
  sender.endRefreshing()
}
sudayn
  • 1,169
  • 11
  • 14
-3

You can simply create an instance of the refresh control and add it at the top of the scroll view. then, in the delegate methods you adjust its behavior to your requirements.

Alex Zavatone
  • 4,106
  • 36
  • 54
Gianluca Tranchedone
  • 3,598
  • 1
  • 18
  • 33