Make scrollbar always visible on UIScrollView?

35,406

Solution 1

Apple indirectly discourage constantly displaying scroll indicators in their iOS Human Interface Guidelines but guidelines are just guidelines for a reason, they don't account for every scenario and sometimes you may need to politely ignore them.

The scroll indicators of any content views are UIImageView subviews of those content views. This means you can access the scroll indicators of a UIScrollView as you would any of its other subviews (i.e. myScrollView.subviews) and modify the scroll indicators as you would any UIImageView (e.g. scrollIndicatorImageView.backgroundColor = [UIColor redColor];).

The most popular solution appears to be the following code:

#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914

@implementation UIImageView (ForScrollView) 

- (void) setAlpha:(float)alpha {

    if (self.superview.tag == noDisableVerticalScrollTag) {
        if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
            if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
                UIScrollView *sc = (UIScrollView*)self.superview;
                if (sc.frame.size.height < sc.contentSize.height) {
                    return;
                }
            }
        }
    }

    if (self.superview.tag == noDisableHorizontalScrollTag) {
        if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
            if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
                UIScrollView *sc = (UIScrollView*)self.superview;
                if (sc.frame.size.width < sc.contentSize.width) {
                    return;
                }
            }
        }
    }

    [super setAlpha:alpha];
}

@end

Which is originally credited to this source.

This defines a category for UIImageView that defines a custom setter for the alpha property. This works because at some point in the underlying code for the UIScrollView, it will set its scroll indicator's alpha property to 0 in order to hide it. At this point it will run through our category and, if the hosting UIScrollView has the right tag, it will ignore the value being set, leaving it displayed.

In order to use this solution ensure your UIScrollView has the appropriate tag e.g. Tag

If you want to display the scroll indicator from the moment its UIScrollView is visible simply flash the scroll indicators when the view appears .e.g

- (void)viewDidAppear:(BOOL)animate
{
    [super viewDidAppear:animate];
    [self.scrollView flashScrollIndicators];
}

Additional SO references:

Solution 2

I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector). I use swizzling instead. And also I don't need to use tags.

Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    // Do swizzling to turn scroll indicator always on
    // Search correct subview with scroll indicator image across tableView subviews
    for (UIView * view in self.categoriesTableView.subviews) {
        if ([view isKindOfClass:[UIImageView class]]) {
            if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
                if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
                    if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
                        // Swizzle class for found imageView, that should be scroll indicator
                        object_setClass(view, [AlwaysOpaqueImageView class]);
                        break;
                    }
                }
            }
        }
    }
    // Ask to flash indicator to turn it on
   [self.categoriesTableView flashScrollIndicators];
}

Add new class

@interface AlwaysOpaqueImageView : UIImageView
@end

@implementation AlwaysOpaqueImageView

- (void)setAlpha:(CGFloat)alpha {
    [super setAlpha:1.0];
}

@end

The scroll indicator (vertical scroll indicator in this case) will be always at the screen.

Update November, 2019

Starting from iOS 13 UIScrollView subclasses are changed. Now scroll indicators are inherited from UIView and has their own private class called _UIScrollViewScrollIndicator. This means, that they are not subclasses of UIImageView now, so old method won't work anymore.

Also we are not able to implement subclass of _UIScrollViewScrollIndicator because it is private class and we don't have access to it. So the only solution is to use runtime. Now to have support for iOS 13 and earlier implement the next steps:

  1. Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    // Do swizzling to turn scroll indicator always on
    // Search correct subview with scroll indicator image across tableView subviews
    for (UIView * view in self.categoriesTableView.subviews) {
        if ([view isKindOfClass:[UIImageView class]]) {
            if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
                if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
                    if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
                        // Swizzle class for found imageView, that should be scroll indicator
                        object_setClass(view, [AlwaysOpaqueImageView class]);
                        break;
                    }
                }
            }
        } else if ([NSStringFromClass(view.class) isEqualToString:@"_UIScrollViewScrollIndicator"]) {
            if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
                if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
                    // Swizzle class for found scroll indicator, (be sure to create AlwaysOpaqueScrollIndicator in runtime earlier!)
                    // Current implementation is in AlwaysOpaqueScrollTableView class
                    object_setClass(view, NSClassFromString(@"AlwaysOpaqueScrollIndicator"));
                    break;
                }
            }
        }
    }
    // Ask to flash indicator to turn it on
    [self.categoriesTableView flashScrollIndicators];
}
  1. Add new class (this is for iOS earlier than 13)
@interface AlwaysOpaqueImageView : UIImageView
@end

@implementation AlwaysOpaqueImageView

- (void)setAlpha:(CGFloat)alpha {
    [super setAlpha:1.0];
}

@end
  1. Add these methods somewhere in you code (either the same view controller as in step 1, or to the desired UIScrollView subclass).
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Create child class from _UIScrollViewScrollIndicator since it is private
        Class alwaysOpaqueScrollIndicatorClass =  objc_allocateClassPair(NSClassFromString(@"_UIScrollViewScrollIndicator"), "AlwaysOpaqueScrollIndicator", 0);
        objc_registerClassPair(alwaysOpaqueScrollIndicatorClass);

        // Swizzle setAlpha: method of this class to custom
        Class replacementMethodClass = [self class];

        SEL originalSelector = @selector(setAlpha:);
        SEL swizzledSelector = @selector(alwaysOpaque_setAlpha:);

        Method originalMethod = class_getInstanceMethod(alwaysOpaqueScrollIndicatorClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(replacementMethodClass, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(alwaysOpaqueScrollIndicatorClass,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(alwaysOpaqueScrollIndicatorClass,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)alwaysOpaque_setAlpha:(CGFloat)alpha {
    [self alwaysOpaque_setAlpha:1.0];
}

This step creates the subclass of _UIScrollViewScrollIndicator called AlwaysOpaqueScrollIndicator in runtime and swizzle setAlpha: method implementation to alwaysOpaque_setAlpha:.

Do not forget to add

#import <objc/runtime.h>

to the files you've inserted this code. Thanks to @Smartcat for reminder about this

Share:
35,406

Related videos on Youtube

Honey
Author by

Honey

Updated on November 02, 2021

Comments

  • Honey
    Honey over 2 years

    I need to make a scrollbar always visible on viewDidLoad so that the user can understand that there is content to scroll. I did the following:

    [myscrollView flashScrollIndicators];
    

    But then the scrollbars only appear for some time after viewDidLoad and disappear again only to reappear when the user touches the screen..

    I need to make scrollbars always visible. How can I do it?

    • IronManGill
      IronManGill over 11 years
      Cudnt understand u , are the scrollviews appearing on touch ?? Also how are the scrollbars disappearing ? Have u put a timer or are u going to the next view and coming back then ??
    • Honey
      Honey over 11 years
      yes the scrollviews are appearing on touch.when I use [myscrollView flashScrollIndicators]; they are appeared on viewLoad and then disappeared after a fraction of seconds.I want to make them alwayz visible ..No im not going back and fro.
    • Divya
      Divya over 11 years
  • Honey
    Honey over 11 years
    Actually in the example u gave I have checked it out.It is regarding changing colours of scrollbars.But I need them to be displayed constantly (always visible ) on the view
  • ipraba
    ipraba over 11 years
    i know.. jst a hint to get that view. Since you get the scrolbar object.. just manipulate with it by adjusting the hidden property or alpha value.. so that it will be show all the time.
  • Honey
    Honey over 11 years
    I tried for hidden property.but that's not working..Can u please tell me how can I do it using alpha value ?
  • ipraba
    ipraba over 11 years
    FYI. This can be just try for you. Since you get the scrollbar object. Try modifying it. But all the property will get modified when there is a action happend in scrollbar. So make modifications to scrollbar at the end of your actions.
  • lxt
    lxt over 10 years
    I would caveat this by saying it relies on a private implementation detail (that the indicators are UIImageView objects) which may not always be true. In fact, it's sod's law that this will invariably break when you least expect it, normally on a new iOS release. So if you do use this, bear in mind your mileage may vary.
  • Sarah Elan
    Sarah Elan over 8 years
    To avoid problems on 64-bit, use (CGFloat)alpha instead of (float)alpha. See stackoverflow.com/q/20855176/1873374.
  • Jacob Boyd
    Jacob Boyd over 7 years
    Using iOS 10, this works. If you want to set the horizontal scroll bar use: UIViewAutoresizing.flexibleTopMargin(Swift 3.0) or UIViewAutoresizingFlexibleTopMargin (Obj-C)
  • Smartcat
    Smartcat over 6 years
    Worked great in iOS 11. Need to #include <objc/runtime.h>.