14

I've got a UIScrollView in my app and I have seen in some other apps that when the user scrolls, the top section fades out on scroll rather than just dissapearing out.

I really love this effect and want to achieve it. Any ideas how it can be done?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Omar
  • 979
  • 3
  • 16
  • 26

6 Answers6

23

Simple 2020 solution:

import UIKit

class FadeTail: UIIView {
    
    private lazy var gradientLayer: CAGradientLayer = {
        let l = CAGradientLayer()
        l.startPoint = CGPoint(x: 0.5, y: 0)
        l.endPoint = CGPoint(x: 0.5, y: 1)
        let baseColor = UIColor.white // for example
        l.colors = [
            baseColor.withAlphaComponent(0),
            baseColor.withAlphaComponent(1),
        ].map{$0.cgColor}
        layer.addSublayer(l)
        return l
    }()
    
    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }
}

Simply

  • autolayout that view in storyboard,
  • any shape or size you wish
  • on top of the view (text, image, webview, anything) you wish to be faded.

Easy.

enter image description here

Tip - gradients, circles, etc

If you need crazy circular/banded/etc fades, use the techniques here: https://stackoverflow.com/a/61174086/294884

enter image description here

Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Probably intuitive for most people... but make sure to set the view's `backgroundColor` to clear (either in the storyboard or programmatically) so the gradient can do its work. – Eric33187 Feb 22 '21 at 06:38
13

EDIT: I've put this code up on github, see here.


See my answer to a similar question.

My solution is to subclass UIScrollView, and create a mask layer in the layoutSubviews method.

#import "FadingScrollView.h"
#import <QuartzCore/QuartzCore.h>

static float const fadePercentage = 0.2;

@implementation FadingScrollView

// ...

- (void)layoutSubviews
{
    [super layoutSubviews];

    NSObject * transparent = (NSObject *) [[UIColor colorWithWhite:0 alpha:0] CGColor];
    NSObject * opaque = (NSObject *) [[UIColor colorWithWhite:0 alpha:1] CGColor];

    CALayer * maskLayer = [CALayer layer];
    maskLayer.frame = self.bounds;

    CAGradientLayer * gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = CGRectMake(self.bounds.origin.x, 0,
                                     self.bounds.size.width, self.bounds.size.height);

    gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque,
                            opaque, transparent, nil];

    // Set percentage of scrollview that fades at top & bottom
    gradientLayer.locations = [NSArray arrayWithObjects:
                               [NSNumber numberWithFloat:0],
                               [NSNumber numberWithFloat:fadePercentage],
                               [NSNumber numberWithFloat:1.0 - fadePercentage],
                               [NSNumber numberWithFloat:1], nil];

    [maskLayer addSublayer:gradientLayer];
    self.layer.mask = maskLayer;
}

@end

The code above fades the top and bottom of the UIScrollView from the background colour to transparent, but this can be easily changed to fade the top only (or fade to any colour you want).

Change this line to fade the top only:

// Fade top of scrollview only
gradientLayer.locations = [NSArray arrayWithObjects:
                           [NSNumber numberWithFloat:0],
                           [NSNumber numberWithFloat:fadePercentage],
                           [NSNumber numberWithFloat:1],
                           [NSNumber numberWithFloat:1], nil];

EDIT 2:

Or fade the top only by changing these two lines:

// Fade top of scrollview only
    gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque, nil];

    gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0],
                                                         [NSNumber numberWithFloat:fadePercentage], nil];

Or, fade the bottom only:

// Fade bottom of scrollview only
    gradientLayer.colors = [NSArray arrayWithObjects: opaque, transparent, nil];

    gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:1.0 - fadePercentage],
                                                         [NSNumber numberWithFloat:1], nil];
Community
  • 1
  • 1
Steph Sharp
  • 11,462
  • 5
  • 44
  • 81
  • Any idea how to fade the bottom? – easythrees Feb 17 '14 at 01:54
  • This seems to mask everything under ios6 – Jiho Kang Jul 04 '14 at 05:43
  • @JihoKang Have you tried my demo project [here](https://github.com/stephsharp/SSFadingScrollView)? It works as expected on iOS 6. – Steph Sharp Jul 04 '14 at 07:11
  • @StephSharp Strange, I've implemented this on a UICollectionView and it doesn't seem to behave like it should. Works perfectly for iOS7 but it just masks everything on iOS6. I'll look into this a bit more there might be a bug in my code. – Jiho Kang Jul 04 '14 at 09:16
  • @StephSharp CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = CGRectMake(self.bounds.origin.x, 260, self.bounds.size.width, self.bounds.size.height-260); gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque, opaque, transparent, nil]; gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0], [NSNumber numberWithFloat:fadePercentage], nil]; This is what I'm using, I need the fade to cover half the screen and this works for iOS7 but not for iOS6. – Jiho Kang Jul 04 '14 at 09:23
  • do you have any idea on how to apply the gradient to just a uitableview and the uitableviewcells but not any floating buttons I add to the tableview? I dont want the floating buttons to be faded but the cell underneath the buttons to be – SleepsOnNewspapers Feb 04 '15 at 22:55
9

You can use a CAGradientLayer by

  1. Adding the QuartzCore.framework to your project (see Linking to Library or Framework).

  2. Add #import of the QuartzCore headers:

    #import <QuartzCore/QuartzCore.h>
    
  3. And then use CAGradientLayer:

    - (void)addGradientMaskToView:(UIView *)view
    {
        CAGradientLayer *gradient = [CAGradientLayer layer];
        gradient.frame = view.bounds;
        gradient.colors = @[(id)[[UIColor clearColor] CGColor], (id)[[UIColor whiteColor] CGColor]];
        gradient.startPoint = CGPointMake(0.5, 0.0); // this is the default value, so this line is not needed
        gradient.endPoint = CGPointMake(0.5, 0.20);
    
        [view.layer setMask:gradient];
    }
    

Note, this CAGradientLayer is a gradient from a color with alpha of 0.0 (e.g. clearColor) to a color to a color with alpha of 1.0 (e.g. whiteColor), not just from black to white. You can adjust the startPoint (the default value is probably fine) and the endPoint to adjust where you want the gradient to be applied.

And generally, when doing this with a UIScrollView, unless you want the gradient to scroll with you, you make the UIScrollView a subview of some other UIView and apply this gradient to that container view, not the scroll view itself.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
5

Thanks to Fattie's answer I created the following UIView extension, in Swift, that takes care of the gradient fading and provides more styles (bottom, top, left right, vertical and horizontal) as well as fade percentage.

For any comments/recommendations, please let me know at the gist I've created, I try to keep both that and this answer up to date with any changes I add.

The Extension:

extension UIView {

    enum UIViewFadeStyle {
        case bottom
        case top
        case left
        case right

        case vertical
        case horizontal
    }

    func fadeView(style: UIViewFadeStyle = .bottom, percentage: Double = 0.07) {
        let gradient = CAGradientLayer()
        gradient.frame = bounds
        gradient.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]

        let startLocation = percentage
        let endLocation = 1 - percentage

        switch style {
        case .bottom:
            gradient.startPoint = CGPoint(x: 0.5, y: endLocation)
            gradient.endPoint = CGPoint(x: 0.5, y: 1)
        case .top:
            gradient.startPoint = CGPoint(x: 0.5, y: startLocation)
            gradient.endPoint = CGPoint(x: 0.5, y: 0.0)
        case .vertical:
            gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
            gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
            gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
            gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]

        case .left:
            gradient.startPoint = CGPoint(x: startLocation, y: 0.5)
            gradient.endPoint = CGPoint(x: 0.0, y: 0.5)
        case .right:
            gradient.startPoint = CGPoint(x: endLocation, y: 0.5)
            gradient.endPoint = CGPoint(x: 1, y: 0.5)
        case .horizontal:
            gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
            gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
            gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
            gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]
        }

        layer.mask = gradient
    }

}
Lucas P.
  • 4,282
  • 4
  • 29
  • 50
  • Thank you @Fattie I've also added a fade percent param, since I needed different amounts of fading for my project. Edited my answer with the change. – Lucas P. Jan 19 '19 at 14:13
2

You add an alpha mask layer to a view containing your scroll view like this:

CALayer *mask = [CALayer layer];
CGImageRef maskRef = [UIImage imageNamed:@"scrollMask"].CGImage;
CGImageRef maskImage = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                             CGImageGetHeight(maskRef),
                                             CGImageGetBitsPerComponent(maskRef),
                                             CGImageGetBitsPerPixel(maskRef),
                                             CGImageGetBytesPerRow(maskRef),
                                             CGImageGetDataProvider(maskRef), NULL, false);
mask.contents = (__bridge id)maskImage;
mask.frame = CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height);
view.layer.mask = mask;
view.layer.masksToBounds = YES;
CGImageRelease(maskImage);

where "scrollMask" is a grayscale image defining mask region: white == fully masked, black == not masked at all and gray == partially masked.

To create the effect you're looking for, the mask image would be black with a white gradient at the top like this:

enter image description here

For more details, take a look at the documentation for CGImageMaskCreate.

Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
1

We built onto Steph Sharp's code to only fade when necessary. Our code is setup as a static utility method instead of a subclass so that we could reuse the method in our subclasses of UIScrollView and UITableView. Here is our static utility method:

#define kScrollViewFadeColorLight [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:0.0]
#define kScrollViewFadeColorDark [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:1.0]

#define kScrollViewScrollBarWidth 7.0
#define kScrollViewFadingEdgeLength 40.0

+ (void) applyFadeToScrollView:(UIScrollView*) scrollView {
  CGFloat topOffset = -scrollView.contentInset.top;
  CGFloat bottomOffset = scrollView.contentSize.height + scrollView.contentInset.bottom - scrollView.bounds.size.height;
  CGFloat distanceFromTop = scrollView.contentOffset.y - topOffset;
  CGFloat distanceFromBottom = bottomOffset - scrollView.contentOffset.y;
  BOOL isAtTop = distanceFromTop < 1.0;
  BOOL isAtBottom = distanceFromBottom < 1.0;
  if (isAtTop && isAtBottom) {
    // There is no scrolling to be done here, so don't fade anything!
    scrollView.layer.mask = nil;
    return;
  }
  NSObject* transparent = (NSObject*)[kScrollViewFadeColorLight CGColor];
  NSObject* opaque = (NSObject*)[kScrollViewFadeColorDark CGColor];

  CALayer* maskLayer = [CALayer layer];
  maskLayer.frame = scrollView.bounds;

  CALayer* scrollGutterLayer = [CALayer layer];
  scrollGutterLayer.frame = CGRectMake(scrollView.bounds.size.width - kScrollViewScrollBarWidth, 0.0,
                                       kScrollViewScrollBarWidth, scrollView.bounds.size.height);
  scrollGutterLayer.backgroundColor = (__bridge CGColorRef)(opaque);
  [maskLayer addSublayer:scrollGutterLayer];

  CAGradientLayer* gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = CGRectMake(0.0, 0.0, scrollView.bounds.size.width, scrollView.bounds.size.height);
  CGFloat fadePercentage = kScrollViewFadingEdgeLength / scrollView.bounds.size.height;
  NSMutableArray* colors = [[NSMutableArray alloc] init];
  NSMutableArray* locations = [[NSMutableArray alloc] init];
  if (!isAtTop) {
    [colors addObjectsFromArray:@[transparent, opaque]];
    [locations addObjectsFromArray:@[@0.0, [NSNumber numberWithFloat:fadePercentage]]];
  }
  if (!isAtBottom) {
    [colors addObjectsFromArray:@[opaque, transparent]];
    [locations addObjectsFromArray:@[[NSNumber numberWithFloat:1.0 - fadePercentage], @1.0]];
  }
  gradientLayer.colors = colors;
  gradientLayer.locations = locations;
  [maskLayer addSublayer:gradientLayer];
  scrollView.layer.mask = maskLayer;
}

Here is our usage of that method.

FadingScrollView.h

@interface FadingScrollView : UIScrollView
@end

FadingScrollView.m

@implementation FadingScrollView
- (void) layoutSubviews {
  [super layoutSubviews];
  [Utils applyFadeToScrollView:self];
}
@end

FadingTableView.h

@interface FadingTableView : UITableView    
@end

FadingTableView.m

@implementation FadingTableView
- (void) layoutSubviews {
  [super layoutSubviews];
  [Utils applyFadeToScrollView:self];
}
@end
Dave Fisher
  • 1,223
  • 8
  • 9