Can I use a UIRefreshControl in a UIScrollView? - ios

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 the UIScrollview down very slowly.
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!!!

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.

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.

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

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.

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 ();
});
});
}

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)
}
}
}
}

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()
}

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)
}
}
}
}

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.

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()
}

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.

Related

Swift UIRefreshControl not working on iPhone 8

I have an UIRefreshControl which works on every device except iPhone 8 (works neither on a physical device or an emulator).
On an iPhone 8 the refresh indicator stops at 3/4 to the end and never calls refresh
This is how it looks like when you pull it all the way down:
Creating the refresh control:
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(TeamViewController.handleRefresh(_:)),
for: UIControlEvents.valueChanged)
refreshControl.tintColor = UIColor.red
return refreshControl
}()
Adding to the view:
if #available(iOS 10.0, *) {
myCollectionView.refreshControl = refreshControl
} else {
myCollectionView.addSubview(refreshControl)
}
I had the same problem and was caused by me setting the view controller to freeform in the storyboard as the simulated size. Changing it back to fixed resolved the issue.
Can also be fixed by using:
override func viewDidLayoutSubviews() {
refreshControl?.didMoveToSuperview()
super.viewDidLayoutSubviews()
}
UIRefreshControl needs to be pulled down too far

Replacing a View with a UIActivityIndicator Programatically

I have been trying to create a quick method that will allow me to replace any UIView with a spinner right before a process starts and do the re-show the view once my process is done. For some reason, the UIView does disappear but the spinner never shows. These are the methods in question:
func showLoader(view: UIView, controller: UIViewController) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.color = AC_BLUE
spinner.center = view.center
spinner.startAnimating()
view.alpha = 0
controller.view.addSubview(spinner)
return spinner
}
func hideLoader(view: UIView, spinner: UIActivityIndicatorView) {
view.alpha = 1
spinner.removeFromSuperview()
}
..which I call with something like this:
let spinner = Extensions().showLoader(view: signInBtn, controller: self)
APICalls().getUser(email: usernameTextField.text!, pass: passwordTextField.text!) { success, user in
//..CODE..//
Extensions().hideLoader(view: self.signInBtn, spinner: spinner)
}
Also, I tried centering on the main VC view, and that does work. So I'm guessing it must be related to the view's position.
Thanks!
Try setting this before adding the spinner to the controllers view (instead of the old spinner.center = view.center):
spinner.center = view.superview!.convert(view.center, to: controller.view)
You need to convert the view's center to the coordinates of the controller.view.

UIBarButtonItem: How can I find its frame?

I have a button in a toolbar. How can I grab its frame? Do UIBarButtonItems not have a frame property?
Try this one;
UIBarButtonItem *item = ... ;
UIView *view = [item valueForKey:#"view"];
CGFloat width;
if(view){
width=[view frame].size.width;
}
else{
width=(CGFloat)0.0 ;
}
This way works best for me:
UIView *targetView = (UIView *)[yourBarButton performSelector:#selector(view)];
CGRect rect = targetView.frame;
With Swift, if you needs to often work with bar button items, you should implement an extension like this:
extension UIBarButtonItem {
var frame: CGRect? {
guard let view = self.value(forKey: "view") as? UIView else {
return nil
}
return view.frame
}
}
Then in your code you can access easily:
if let frame = self.navigationItem.rightBarButtonItems?.first?.frame {
// do whatever with frame
}
Oof, lots of rough answers in this thread. Here's the right way to do it:
import UIKit
class ViewController: UIViewController {
let customButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
customButton.setImage(UIImage(named: "myImage"), for: .normal)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: customButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(self.customButton.convert(self.customButton.frame, to: nil))
}
}
Thanks to Anoop Vaidya for the suggested answer. An alternative could be (providing you know the position of the button in the toolbar)
UIView *view= (UIView *)[self.toolbar.subviews objectAtIndex:0]; // 0 for the first item
CGRect viewframe = view.frame;
Here's what I'm using in iOS 11 & Swift 4. It could be a little cleaner without the optional but I'm playing it safe:
extension UIBarButtonItem {
var view: UIView? {
return perform(#selector(getter: UIViewController.view)).takeRetainedValue() as? UIView
}
}
And usage:
if let barButtonFrame = myBarButtonItem.view?.frame {
// etc...
}
Edit: I don't recommend using this anymore. I ended up changing my implementation to use UIBarButtonItems with custom views, like Dan's answer
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
UIView *view = [item valueForKey:#"view"];
return [view frame];
}
You can create a UIBarButtonItem with a custom view, which is a UIButton, then you can do whatever you want. :]
in Swift 4.2 and inspired with luca
extension UIBarButtonItem {
var frame:CGRect?{
return (value(forKey: "view") as? UIView)?.frame
}
}
guard let frame = self.navigationItem.rightBarButtonItems?.first?.frame else{ return }
You can roughly calculate it by using properties like layoutMargins and frame on the navigationBar, combined with icon size guides from Human Interface Guidelines and take into count the current device orientation:
- (CGRect)rightBarButtonFrame {
CGFloat imageWidth = 28.0;
CGFloat imageHeight = UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeLeft || UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeRight ? 18.0 : 28.0;
UIEdgeInsets navigationBarLayoutMargins = self.navigationController.navigationBar.layoutMargins;
CGRect navigationBarFrame = self.navigationController.navigationBar.frame;
return CGRectMake(navigationBarFrame.size.width-(navigationBarLayoutMargins.right + imageWidth), navigationBarFrame.origin.y + navigationBarLayoutMargins.top, imageWidth, imageHeight);
}
Try this implementation:
#implementation UIBarButtonItem(Extras)
- (CGRect)frameInView:(UIView *)v {
UIView *theView = self.customView;
if (!theView.superview && [self respondsToSelector:#selector(view)]) {
theView = [self performSelector:#selector(view)];
}
UIView *parentView = theView.superview;
NSArray *subviews = parentView.subviews;
NSUInteger indexOfView = [subviews indexOfObject:theView];
NSUInteger subviewCount = subviews.count;
if (subviewCount > 0 && indexOfView != NSNotFound) {
UIView *button = [parentView.subviews objectAtIndex:indexOfView];
return [button convertRect:button.bounds toView:v];
} else {
return CGRectZero;
}
}
#end
You should do a loop over the subviews and check their type or their contents for identifying.
It is not safe to access view by kvo and you cannot be sure about the index.
Check out this answer: How to apply borders and corner radius to UIBarButtonItem? which explains how to loop over subviews to find the frame of a button.
I used a view on the bar button item with a tag on the view:
for view in bottomToolbar.subviews {
if let stackView = view.subviews.filter({$0 is UIStackView}).first {
//target view has tag = 88
if let targetView = stackView.subviews.filter({$0.viewWithTag(88) != nil}).first {
//do something with target view
}
}
}
Swift 4 up The current best way to do it is to access its frame from :
self.navigationItem.rightBarButtonItems by
let customView = navigationItem.rightBarButtonItems?.first?.customView // access the first added customView
Accessing this way is safer than accessing private api.
check out the answer in this :
After Add a CustomView to navigationItem, CustomView always return nil

ios Changing UIScrollView scrollbar color to different colors

How can we change color of UIScrollview's scroll indicator to something like blue, green etc.
I know we can change it to white, black. But other then these colors.
Many Thanks
Unfortunately you can't, of course you can always roll your own. These are your options:
UIScrollViewIndicatorStyleDefault:
The default style of scroll indicator, which is black with a white border. This style is good against any content background.
UIScrollViewIndicatorStyleBlack:
A style of indicator which is black and smaller than the default style. This style is good against a white content background.
UIScrollViewIndicatorStyleWhite:
A style of indicator is white and smaller than the default style. This style is good against a black content background.
Here's more safe Swift 3 method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let verticalIndicator = scrollView.subviews.last as? UIImageView
verticalIndicator?.backgroundColor = UIColor.green
}
Both UIScrollView indicator are sub view of UIScrollView. So, we can
access subview of UIScrollView and change the property of subview.
1 .Add UIScrollViewDelegate
#interface ViewController : UIViewController<UIScrollViewDelegate>
#end
2. Add scrollViewDidScroll in implementation section
-(void)scrollViewDidScroll:(UIScrollView *)scrollView1
{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
//set color to vertical indicator
[verticalIndicator setBackgroundColor:[UIColor redColor]];
//get refrence of horizontal indicator
UIImageView *horizontalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-2)]);
//set color to horizontal indicator
[horizontalIndicator setBackgroundColor:[UIColor blueColor]];
}
Note:- Because these indicator update every time when you scroll
(means reset to default). SO, we put this code in scrollViewDidScroll
delegate method.
Demo available on GitHub - https://github.com/developerinsider/UIScrollViewIndicatorColor
Based on the answer of #Alex (https://stackoverflow.com/a/58415249/3876285), I'm posting just a little improvement to change the color of scroll indicators.
extension UIScrollView {
var scrollIndicators: (horizontal: UIView?, vertical: UIView?) {
guard self.subviews.count >= 2 else {
return (horizontal: nil, vertical: nil)
}
func viewCanBeScrollIndicator(view: UIView) -> Bool {
let viewClassName = NSStringFromClass(type(of: view))
if viewClassName == "_UIScrollViewScrollIndicator" || viewClassName == "UIImageView" {
return true
}
return false
}
let horizontalScrollViewIndicatorPosition = self.subviews.count - 2
let verticalScrollViewIndicatorPosition = self.subviews.count - 1
var horizontalScrollIndicator: UIView?
var verticalScrollIndicator: UIView?
let viewForHorizontalScrollViewIndicator = self.subviews[horizontalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForHorizontalScrollViewIndicator) {
horizontalScrollIndicator = viewForHorizontalScrollViewIndicator.subviews[0]
}
let viewForVerticalScrollViewIndicator = self.subviews[verticalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForVerticalScrollViewIndicator) {
verticalScrollIndicator = viewForVerticalScrollViewIndicator.subviews[0]
}
return (horizontal: horizontalScrollIndicator, vertical: verticalScrollIndicator)
}
}
If you don't add .subviews[0], you will get the deeper view and when you try to change the color of the indicator, this will appear with a weird white effect. That's because there is another view in front of it:
By adding .subviews[0] to each indicator view, once you try to change the color by calling:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async() {
scrollView.scrollIndicators.vertical?.backgroundColor = UIColor.yourcolor
}
}
You will access to the first view and change the color properly:
Kudos to #Alex who posted a great solution 👍
in IOS 13
Try this one
func scrollViewDidScroll(_ scrollView: UIScrollView){
if #available(iOS 13, *) {
(scrollView.subviews[(scrollView.subviews.count - 1)].subviews[0]).backgroundColor = UIColor.themeColor(1.0) //verticalIndicator
(scrollView.subviews[(scrollView.subviews.count - 2)].subviews[0]).backgroundColor = UIColor.themeColor(1.0) //horizontalIndicator
} else {
if let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as? UIImageView) {
verticalIndicator.backgroundColor = UIColor.themeColor(1.0)
}
if let horizontalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 2)] as? UIImageView) {
horizontalIndicator.backgroundColor = UIColor.themeColor(1.0)
}
}
}
Swift 2.0 :
Add UIScrollView Delegate.
func scrollViewDidScroll(scrollView: UIScrollView){
let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as! UIImageView)
verticalIndicator.backgroundColor = UIColor.greenColor()
let horizontalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 2)] as! UIImageView)
horizontalIndicator.backgroundColor = UIColor.blueColor()
}
Try this it would certainly help you
for ( UIView *view in scrollBar.subviews ) {
if (view.tag == 0 && [view isKindOfClass:UIImageView.class])
{
UIImageView *imageView = (UIImageView *)view;
imageView.backgroundColor = [UIColor yellowColor];
}
}
Explanation: UIScrollBar is a collection of subviews. Here scrollBar indicator(vertical/horizontal) is the one of the subviews and it's an UIImageView.So if we set custom color to the UIImageView it effects scrollBar Indicator.
You can change an image of indicator, but you should do this repeadeatly
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.chageScrollIndicator()
}
func chageScrollIndicator (){
if let indicator = self.collection.subviews.last as? UIImageView {
let edge = UIEdgeInsets(top: 1.25,
left: 0,
bottom: 1.25,
right: 0)
indicator.image = UIImage(named: "ScrollIndicator")?.withRenderingMode(.alwaysTemplate).resizableImage(withCapInsets: edge)
indicator.tintColor = UIConfiguration.textColor
}
}
You can use this 2 image as template:
in IOS 13
Since iOS13 scroll indicators have class _UIScrollViewScrollIndicator, not UIImageView.
Many people used code like
let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as! UIImageView)
It's not good idea, because they promised that last subview will be UIImageView :). Now it's not and they can get crash.
You can try following code to get scrollView indicators:
extension UIScrollView {
var scrollIndicators: (horizontal: UIView?, vertical: UIView?) {
guard self.subviews.count >= 2 else {
return (horizontal: nil, vertical: nil)
}
func viewCanBeScrollIndicator(view: UIView) -> Bool {
let viewClassName = NSStringFromClass(type(of: view))
if viewClassName == "_UIScrollViewScrollIndicator" || viewClassName == "UIImageView" {
return true
}
return false
}
let horizontalScrollViewIndicatorPosition = self.subviews.count - 2
let verticalScrollViewIndicatorPosition = self.subviews.count - 1
var horizontalScrollIndicator: UIView?
var verticalScrollIndicator: UIView?
let viewForHorizontalScrollViewIndicator = self.subviews[horizontalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForHorizontalScrollViewIndicator) {
horizontalScrollIndicator = viewForHorizontalScrollViewIndicator
}
let viewForVerticalScrollViewIndicator = self.subviews[verticalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForVerticalScrollViewIndicator) {
verticalScrollIndicator = viewForVerticalScrollViewIndicator
}
return (horizontal: horizontalScrollIndicator, vertical: verticalScrollIndicator)
}
}
If you need only one (h or v indicator) - it's better to cut this func and keep only one you need (to improve perfomance).
Also it would be good to call update func inside of DispatchQueue, to keep smoothness of scrolling.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
scrollView.updateCustomScrollIndicatorView()
}
}
This is how the color of the scroll bar is changed:
//scroll view
UIScrollView *scView = [[UIScrollView alloc] init];
scView.frame = self.view.bounds; //scroll view occupies full parent views
scView.contentSize = CGSizeMake(400, 800);
scView.backgroundColor = [UIColor lightGrayColor];
scView.indicatorStyle = UIScrollViewIndicatorStyleBlack;
scView.showsHorizontalScrollIndicator = NO;
scView.showsVerticalScrollIndicator = YES;
scView.scrollEnabled = YES;
[self.view addSubview: scView];
If you wish to add image as well, here is the code for Swift 3
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let verticalIndicator = scrollView.subviews.last as? UIImageView
verticalIndicator?.image = UIImage(named: "imageName")
}
This works for UITableView and UICollectionView as well.
I wrote an article about this not so far ago. Unfortunately color of this bars defined by pre-defined images, so if you are going to change the color of bars some extra work will be required. Take a look to following link, you will definitely find an answer here since I tried to solve the same issue.
http://leonov.co/2011/04/uiscrollviews-scrollbars-customization/
I ran into the same problem recently so I decided to write a category for it.
https://github.com/stefanceriu/UIScrollView-ScrollerAdditions
[someScrollView setVerticalScrollerTintColor:someColor];
[someScrollView setHorizontalScrollerTintColor:someColor];`
It blends it with the original image so only the color will change. On the other hand, it can also be modified to provide a custom image for the scrollers to use.
Here is what I did in Swift 4, similar to previous answers. In my case I'm recoloring the image to be invisible, set correct corner radius and only execute this process once.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let color = UIColor.red
guard
let verticalIndicator = scrollView.subviews.last as? UIImageView,
verticalIndicator.backgroundColor != color,
verticalIndicator.image?.renderingMode != .alwaysTemplate
else { return }
verticalIndicator.layer.masksToBounds = true
verticalIndicator.layer.cornerRadius = verticalIndicator.frame.width / 2
verticalIndicator.backgroundColor = color
verticalIndicator.image = verticalIndicator.image?.withRenderingMode(.alwaysTemplate)
verticalIndicator.tintColor = .clear
}
please use below code on iOS Renderer
private bool _layouted;
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (!_layouted)
{
this.Layer.BorderColor = UIColor.Red.CGColor;
var Verticalbar = (UIImageView)this.Subviews[this.Subviews.Length - 1];
Verticalbar.BackgroundColor = Color.FromHex("#0099ff").ToUIColor();
var Horizontlebar = (UIImageView)this.Subviews[this.Subviews.Length - 2];
Horizontlebar.BackgroundColor = Color.FromHex("#0099ff").ToUIColor();
_layouted = true;
}
}
As for iOS 13 subviews changed so adding simple if, solved this issues.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0) {
UIView *verticalIndicator = [scrollView.subviews lastObject];
verticalIndicator.backgroundColor = [UIColor redColor];
} else {
UIImageView *verticalIndicator = [scrollView.subviews lastObject];
verticalIndicator.backgroundColor = [UIColor redColor];
}
}
You can use custom UIScrollView scrollBars to implement color in scrollbars. For more details look here

Turn off Zooming in UIScrollView

Does anyone know a way to temporarily turn off zooming when using a UIScrollView?
I see that you can disable scrolling using the following:
self.scrollView.scrollEnabled = false;
but I'm not seeing a similar command for zooming. Any thoughts?
If you want to disable the user's ability to zoom through gestures then in iOS 5 and above you can disable the pinch gesture. This still allows you to control the scroll view from code...
scrollView.pinchGestureRecognizer.enabled = NO;
similarly for pan...
scrollView.panGestureRecognizer.enabled = NO;
This must be called in - (void)viewDidAppear:(BOOL)animated or later as otherwise the system resets it to enabled.
Swift 4.x and above:
imageZoomView.pinchGestureRecognizer?.isEnabled = false / true
Following fbrereto's advice above, I created two functions lockZoom and unlockZoom. When locking Zoom i copied my max and min zoom scales to variables then set the max and min zoom scale to 1.0. Unlocking zoom just reverses the process.
-(void)lockZoom
{
maximumZoomScale = self.scrollView.maximumZoomScale;
minimumZoomScale = self.scrollView.minimumZoomScale;
self.scrollView.maximumZoomScale = 1.0;
self.scrollView.minimumZoomScale = 1.0;
}
-(void)unlockZoom
{
self.scrollView.maximumZoomScale = maximumZoomScale;
self.scrollView.minimumZoomScale = minimumZoomScale;
}
Also you can return "nil" as zooming view in UIScrollViewDelegate:
- (UIView *) viewForZoomingInScrollView:(UIScrollView *) scrollView
{
return canZoom?view:nil;
}
Check setting minimumZoomScale and maximumZoomScale; According to the docs:
maximumZoomScale must be greater than minimumZoomScale for zooming to be enabled.
So, setting the values to be the same should disable zooming.
I have tried setting minimumZoomScale and maximumZoomScale properties of UIScrollView to 1 or isMultipleTouchEnabled property of UIView to false or return nil from viewForZooming(in:) of UIScrollViewDelegate but none worked. In my case, after several trial and error, the following works in my case [Tested on iOS 10.3]:
class MyViewController: UIViewController {
var webView: WKWebView?
override viewDidLoad() {
super.viewDidLoad()
//...
self.webView.scrollView.delegate = self
//...
}
}
extension MyViewController: UIScrollViewDelegate {
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
scrollView.pinchGestureRecognizer?.isEnabled = false
}
}
I know this is a really old question but I made a slight variation for my purposes.
I wanted to be able to easily tell if the zooming was in fact enabled/disabled without relying on a comparison between scrollView.minimumZoomScale == scrollView.maximumZoomScale, which could possibly not reflect whether zooming was actually enabled/disabled.
So I did this
// .h
#property (assign, nonatomic, getter=isZoomEnabled) BOOL zoomEnabled;
// .m
#synthesize zoomEnabled = _zoomEnabled;
- (void)setZoomEnabled:(BOOL)zoomEnabled;
{
_zoomEnabled = zoomEnabled;
UIScrollView *scrollView = self.scrollView;
if (zoomEnabled) {
scrollView.minimumZoomScale = self.minimumZoomScale;
scrollView.maximumZoomScale = self.maximumZoomScale;
} else {
scrollView.minimumZoomScale = 0.0f;
scrollView.maximumZoomScale = 0.0f;
}
}
The values for self.minimumZoomScale and self.maximumZoomScale are set at the time the UIScrollView is configured.
This gives me the ability to set/ask if zooming is enabled.
myViewController.zoomEnabled = YES;
myViewController.isZoomEnabled;
here, my solution for stop zooming on scrollview.
self.scrollView.minimumZoomScale=self.scrollView.maximumZoomScale;
Swift 3 Version:
func lockScrollViewZooming() {
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 1.0
scrollView.bounces = false
scrollView.bouncesZoom = false
// Also, if you have double tap recognizer,
// remember to remove it
scrollView.removeGestureRecognizer(doubleTapGestureRecognizer)
}
func unlockScrollViewZooming() {
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 4.0
scrollView.bounces = true
scrollView.bouncesZoom = true
// Also, if you have double tap recognizer,
// remember to add it
scrollView.removeGestureRecognizer(doubleTapGestureRecognizer)
}
Note that doubleTapGestureRecognizer should be an instance variable. It should be similar to:
private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = {
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
doubleTapGestureRecognizer.delegate = self
return doubleTapGestureRecognizer
}()
#objc private func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
//scrollView.setZoomScale((scrollView.zoomScale > scrollView.minimumZoomScale) ? scrollView.minimumZoomScale : scrollView.maximumZoomScale, animated: true)
if scrollView.zoomScale > scrollView.minimumZoomScale {
scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
} else {
let touchLocation = recognizer.location(in: recognizer.view)
scrollView.zoom(to: CGRect(origin: touchLocation, size: CGSize(width: 22.0, height: 20.0)), animated: true)
}
}
You need to turn off Two Fingers and Double Tap of scroll view
self.scrollView.delegate = self
And
extension YourViewController: UIScrollViewDelegate {
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
scrollView.pinchGestureRecognizer?.isEnabled = false
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return nil
}
}
If you want to disable only zooming with pinch gesture, below code does the trick.
scrollView.pinchGestureRecognizer?.requireGestureRecognizerToFail(scrollView.panGestureRecognizer)

Resources