Can I use a UIRefreshControl in a UIScrollView?
IosIphoneUiscrollviewxamarin.iosUirefreshcontrolIos Problem Overview
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.
-
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 theUIScrollview
down very slowly. -
When I scroll down and initiate the reload, then let go of the
UIScrollView
, theUIScrollView
stays where I let it go. After it is finished reloading, theUIScrollView
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!!!
Ios Solutions
Solution 1 - Ios
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.
Solution 2 - Ios
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.
Solution 3 - Ios
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
Solution 4 - Ios
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
.
Solution 5 - Ios
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 ();
});
});
}
Solution 6 - Ios
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)
}
}
}
}
Solution 7 - Ios
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()
}
Solution 8 - Ios
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)
}
}
}
}
Solution 9 - Ios
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.
Solution 10 - Ios
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()
}
Solution 11 - Ios
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.