BuySellAds.com

Read the chapters in my new book Barcodes with iOS 7 as I hand them in. Great new app opportunities await!
Our DNA is written in Objective-C
Jump

Adding Fading Gradients to UITableView

Masked Table View

Jason Jardim asked (4 Months ago):

This is just a screen shot I found with someone posting a similar question.  I am trying to fade out he top/ bottom cells in a tableview. How do I achieve this effect?

First of all, Jason, I am sorry it took so long. I was extremely busy during the past few months but I kept your e-mail at the bottom of my inbox as something that I am really interested in to give a good answer to.

Let me make it up for you by proposing several solutions to your question as well as show one that I find the coolest.

First of all by looking at the sample screenshot we see that there is a border around the table view. This tells us that the table view does not fill the entire screen but there is obviously an imageView responsible for the ornamentals surrounding the table view.

If we go with this imageView then the next question is whether the image is below or above the table view itself. The first instinct might be “below!” but that can be treacherous and cause you more work than is actually needed to achieve this effect. You could just as well have the part for the table view “cut out” in the image, i.e. have the center portion be Alpha 0 and the top and bottom would be an alpha gradient from 100% to 0%.

A designer versed with Photoshop could easily create something like this for you. Then you put this imageView on top of your table view, size the table view to fit inside the border. Finally you will want to make sure that userInteraction is disabled on the imageView, because otherwise no touches would be reaching the table view.

While this solution is simple it does not satisfy me personally. As developer I want to achieve such an effect entirely in code because then it has the added advantage of being independent or resolution or interface orientation.

The first thing we’ll try is to mask the layer of the table view. On iOS each UIView has a CALayer that takes care of the actual drawing. And each CALayer has a mask property where you can set another layer to mask out parts of the host layer.

For masking layers it does not matter which colors you use because only the alpha value of each pixel is considered for the composition. 100% alpha means that a layer pixel shows fully, 0% alpha causes a layer pixel to be transparent.

For this tutorial I created a new navigation-based app without CoreData. We also need the QuartzCore.framework for the advanced layer handling methods. A useful layer type for this purpose is CAGradientLayer which exists since iOS version 3.0.

RootViewController.h

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
 
@interface RootViewController : UITableViewController
{
    CAGradientLayer *maskLayer;
}
 
@end

For the sake of simplicity we create the mask in viewWillAppear because at this point the table view has already been resized to the proper size. If we would create it earlier (or if you plan to support different sizes) then you would also need code to adjust the layer bounds to fit.

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
 
    if (!maskLayer)
    {
        maskLayer = [CAGradientLayer layer];
 
        CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
        CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
 
        maskLayer.colors = [NSArray arrayWithObjects:(id)outerColor, 
                            (id)innerColor, (id)innerColor, (id)outerColor, nil];
        maskLayer.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], 
                            [NSNumber numberWithFloat:0.2], 
                            [NSNumber numberWithFloat:0.8], 
                            [NSNumber numberWithFloat:1.0], nil];
 
        maskLayer.bounds = CGRectMake(0, 0,
                            self.tableView.frame.size.width,
                            self.tableView.frame.size.height);
        maskLayer.anchorPoint = CGPointZero;
 
       self.view.layer.mask = maskLayer;
    }
}

We only create the layer once. Since there can be only one mask layer at a time we combine the top and the bottom gradient into one where the outer areas will be 20% of the entire height. The default anchor point is the center of the layer, so we change that to the top left.

If we stopped here then you would get the gradients, but they would move together with the contents of the table view. Luckily table views are UIScrollView child classes and thus you can also implement the UIScrollViewDelegate methods. We are using the one that fires on each movement to adjust the position of the masking layer.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    maskLayer.position = CGPointMake(0, scrollView.contentOffset.y);
    [CATransaction commit];
}

Note that we also need to disable actions because position is an animatable property on CALayer. If you set that this triggers an implicit animation which would cause the masking layer to lag behind. With actions disabled the layer position is set right away.

There’s another problem that only becomes apparent if you start scrolling and the vertical scroll bar shows: it is also affected by the mask which you probably don’t want because it looks weird.

So instead of actually use the mask for the regular layer masking we add it as a sublayer. Because now the colors are just composited on top of the table view we have to reverse them or else the inside would be whitened out and only the outside would show.

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
 
    if (!maskLayer)
    {
        maskLayer = [CAGradientLayer layer];
 
        CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
        CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
 
        maskLayer.colors = [NSArray arrayWithObjects:(id)outerColor, 
                            (id)innerColor, (id)innerColor, (id)outerColor, nil];
        maskLayer.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], 
                            [NSNumber numberWithFloat:0.2], 
                            [NSNumber numberWithFloat:0.8], 
                            [NSNumber numberWithFloat:1.0], nil];
 
        maskLayer.bounds = CGRectMake(0, 0,
                            self.tableView.frame.size.width,
                            self.tableView.frame.size.height);
        maskLayer.anchorPoint = CGPointZero;
 
        [self.view.layer addSublayer:maskLayer];
    }
}

This change brought us full circle to essentially adding an imageView on top of the table view to mask out the edge gradients. In this example the rootViewController’s view is a UITableView and so we have no other place to add this sublayer to. But if the table View were itself a subview of some larger view we could add the gradient layer there and not having to deal with the scrolling.

Actually it’s not entirely true that there’s no other layer besides the table view one. You can always move to the view’s superview and add it there, but this is considered bad form, a view should only fiddle with it’s own descendants.

This tutorial has shown how you can employ a CAGradientLayer to block out parts of another view. With the same technique you could for example use a CAShapeLayer to mask out an irregular portion of any view. Or you could use a plain image as contents of a plain CALayer for such an effect.

Yet another possibility – if you want the gradients to be built into the table view itself – would be to subclass UITableView and do the gradient management there. Either use one large gradient or have two UIViews that draw the gradients individually. There you would put the repositioning of the gradients into the layoutSubviews property instead of the scrollview delegate method.

Happy Masking!


Categories: Q&A

%d bloggers like this: