7

I've made a little research about my problem and unfortunately there was no solution for my problem. The closest was Fade UIImageView as it approaches the edges of a UIScrollView but it's still not for me.

I want my table to apply an "invisibility gradient" on the top. If the cell is at a 50px distance from the top edge it starts to vanish. The closer it is to the upper edge, the more invisible the part is. The cells height is about 200 pixels, so the lower part of the cell need to be visible in 100%. But nevermind - I need a table view (or table view container) to do this task, because similar tables can display other cells.

If the table is a subview of a solid color view, I can achieve that by adding an image which is a horizontal gradient that I can streach to any width. The top pixel of that image starts with the exact color of the background, and going down the same color has less alpha.

But... we have a UITableView with transparent color. Below the table there is no solid color, but a pattern image/texture, that can also be different on other screens of the app.

Do you have any Idea how I can achieve this behaviour?

Regards

Community
  • 1
  • 1
Chris Rutkowski
  • 1,774
  • 1
  • 26
  • 36

4 Answers4

14

I took this tutorial and made some changes and additions:

  • It now works on all tableviews - even if they are part of bigger screen.
  • It works regardless of the background or whatever is behind the tableview.
  • The mask changes depends on the position of the table view - when scrolled to top only the bottom faded, in when scrolled to bottom only top is faded...

1. Start by importing QuartzCore and setting a mask layer in your controller:

EDIT: No need for reference to CAGradientLayer in class.

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface mViewController : UIViewController
.
.
@end

2. Add this to viewWillAppear viewDidLayoutSubviews: (See @Darren's comment on this one)

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if (!self.tableView.layer.mask)
    {
        CAGradientLayer *maskLayer = [CAGradientLayer layer];

        maskLayer.locations = @[[NSNumber numberWithFloat:0.0], 
                                [NSNumber numberWithFloat:0.2], 
                                [NSNumber numberWithFloat:0.8], 
                                [NSNumber numberWithFloat:1.0]];

        maskLayer.bounds = CGRectMake(0, 0,
                            self.tableView.frame.size.width,
                            self.tableView.frame.size.height);
        maskLayer.anchorPoint = CGPointZero;

       self.tableView.layer.mask = maskLayer;
    }
    [self scrollViewDidScroll:self.tableView];
}

3. Make sure you are a delegate of UIScrollViewDelegate by adding it in the .h of your controller:

@interface mViewController : UIViewController <UIScrollViewDelegate>

4. To finish, implement scrollViewDidScroll in your controller .m:

#pragma mark - Scroll View Delegate Methods

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
    CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
    NSArray *colors;

    if (scrollView.contentOffset.y + scrollView.contentInset.top <= 0) {
        //Top of scrollView
        colors = @[(__bridge id)innerColor, (__bridge id)innerColor,
                   (__bridge id)innerColor, (__bridge id)outerColor];
    } else if (scrollView.contentOffset.y + scrollView.frame.size.height
               >= scrollView.contentSize.height) {
        //Bottom of tableView
        colors = @[(__bridge id)outerColor, (__bridge id)innerColor,
                   (__bridge id)innerColor, (__bridge id)innerColor];
    } else {
        //Middle
        colors = @[(__bridge id)outerColor, (__bridge id)innerColor,
                   (__bridge id)innerColor, (__bridge id)outerColor];
    }
    ((CAGradientLayer *)scrollView.layer.mask).colors = colors;

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    scrollView.layer.mask.position = CGPointMake(0, scrollView.contentOffset.y);
    [CATransaction commit];
}

Again: most of the solution is from this tutorial in cocoanetics.

Darren
  • 10,182
  • 20
  • 95
  • 162
Aviel Gross
  • 9,770
  • 3
  • 52
  • 62
  • This is great. I would however add the mask setup code to `viewDidLayoutSubviews` and not `viewWillAppear` as depending on sizes, layouts are not fully defined in `viewWillAppear` (On my iPhone 6 it shrunk the tableview). – Darren Nov 10 '14 at 13:01
  • True... I wrote this when this wasn't as important as today and `viewDidLayoutSubviews` does seem like a better place... – Aviel Gross Nov 10 '14 at 13:23
  • @Darren if I only want this to be at the bottom of my UITableView do I change the scroll view delegate methods of should i change the viewDidLayoutSubviews method? – SleepsOnNewspapers Feb 02 '15 at 22:10
  • Switching numberWithFloat:0.2 to 0.0 in viewDidLayoutSubviews should do the trick – Aviel Gross Feb 02 '15 at 22:49
  • 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 23:12
  • Add `[self.view layoutIfNeeded];` right after `if (!self.tableView.layer.mask)` if you're using auto layout (as many are now) to make sure the tableview has been sized correctly. – JCalhoun Jan 23 '16 at 16:46
  • Is there anyway to reduce the height of the gradient? – Supertecnoboff May 31 '18 at 19:24
  • 1
    @Supertecnoboff the values of `maskLayer.locations` in `viewDidLayoutSubviews` are relative to the layer size, ie right now the fade is the top 20% and the bottom 20%. Let's say you would want to fade only 10% at the bottom, you would then change the third value from `0.8` to `0.9` – Aviel Gross Jun 07 '18 at 21:07
  • This is a great solution :D – Stephy Samaniego May 23 '19 at 21:07
6

This is a translation of Aviel Gross's answer to Swift

import UIKit

class mViewController: UIViewController, UIScrollViewDelegate {

    //Emitted boilerplate code

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        if self.tableView.layer.mask == nil {

            //If you are using auto layout
            //self.view.layoutIfNeeded()

            let maskLayer: CAGradientLayer = CAGradientLayer()

            maskLayer.locations = [0.0, 0.2, 0.8, 1.0]
            let width = self.tableView.frame.size.width
            let height = self.tableView.frame.size.height
            maskLayer.bounds = CGRect(x: 0.0, y: 0.0, width: width, height: height)
            maskLayer.anchorPoint = CGPoint.zero

            self.tableView.layer.mask = maskLayer
        }

        scrollViewDidScroll(self.tableView)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let outerColor = UIColor(white: 1.0, alpha: 0.0).cgColor
        let innerColor = UIColor(white: 1.0, alpha: 1.0).cgColor

        var colors = [CGColor]()

        if scrollView.contentOffset.y + scrollView.contentInset.top <= 0 {
            colors = [innerColor, innerColor, innerColor, outerColor]
        } else if scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height {
            colors = [outerColor, innerColor, innerColor, innerColor]
        } else {
            colors = [outerColor, innerColor, innerColor, outerColor]
        }

        if let mask = scrollView.layer.mask as? CAGradientLayer {
            mask.colors = colors

            CATransaction.begin()
            CATransaction.setDisableActions(true)
            mask.position = CGPoint(x: 0.0, y: scrollView.contentOffset.y)
            CATransaction.commit()
        }

    }

    //Emitted boilerplate code
}
Michal Šrůtek
  • 1,647
  • 16
  • 17
Asdrubal
  • 2,421
  • 4
  • 29
  • 37
5

Swift version of @victorfigol's excellent solution:

class FadingTableView : UITableView {
  var percent = Float(0.05)

  private let outerColor = UIColor(white: 1.0, alpha: 0.0).cgColor
  private let innerColor = UIColor(white: 1.0, alpha: 1.0).cgColor

  override func awakeFromNib() {
    super.awakeFromNib()
    addObserver(self, forKeyPath: "bounds", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
  }

  override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if object is FadingTableView && keyPath == "bounds" {
        initMask()
    }
  }

  deinit {
    removeObserver(self, forKeyPath:"bounds")
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    updateMask()
  }

  func initMask() {
    let maskLayer = CAGradientLayer()
    maskLayer.locations = [0.0, NSNumber(value: percent), NSNumber(value:1 - percent), 1.0]
    maskLayer.bounds = CGRect(x:0, y:0, width:frame.size.width, height:frame.size.height)
    maskLayer.anchorPoint = CGPoint.zero
    self.layer.mask = maskLayer

    updateMask()
  }

  func updateMask() {
    let scrollView : UIScrollView = self

    var colors = [CGColor]()

    if scrollView.contentOffset.y <= -scrollView.contentInset.top { // top
        colors = [innerColor, innerColor, innerColor, outerColor]
    }
    else if (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height { // bottom
        colors = [outerColor, innerColor, innerColor, innerColor]
    }
    else {
        colors = [outerColor, innerColor, innerColor, outerColor]
    }

    if let mask = scrollView.layer.mask as? CAGradientLayer {
        mask.colors = colors

        CATransaction.begin()
        CATransaction.setDisableActions(true)
        mask.position = CGPoint(x: 0.0, y: scrollView.contentOffset.y)
        CATransaction.commit()
     }
  }
}
Michal Šrůtek
  • 1,647
  • 16
  • 17
Sherwin Zadeh
  • 1,339
  • 15
  • 17
  • 1
    Force casts, semi colons, not using type inference, conditions in parentheses ... Please review your Swift 3 copy. – Victor Carmouze Jan 09 '17 at 17:32
  • 1
    Got this to work after unwrapping the layer optional instead of forcing it : if let mask = scrollView.layer.mask as? CAGradientLayer { mask.colors = colors } – Alexis C. Mar 24 '17 at 13:25
4

This is my version of the fading table view by inheriting UITableView. Tested for iOS 7 & 8.

  1. There is no need to implement scrollViewDidScroll, layoutSubviews can be used instead.
  2. By observing for bounds changes, the mask is always properly placed when the rotation changes.
  3. You can use the percent parameter to change how much fading you want at the edges, I find a small value looking better in some cases.

CEFadingTableView.m

#import "CEFadingTableView.h"

@interface CEFadingTableView()

@property (nonatomic) float percent; // 1 - 100%

@end

@implementation CEFadingTableView

- (void)awakeFromNib
{
    [super awakeFromNib];

    self.percent = 5.0f;

    [self addObserver:self forKeyPath:@"bounds" options:0 context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if(object == self && [keyPath isEqualToString:@"bounds"])
    {
        [self initMask];
    }
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"bounds"];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    [self updateMask];
}

- (void)initMask
{
    CAGradientLayer *maskLayer = [CAGradientLayer layer];

    maskLayer.locations = @[@(0.0f), @(_percent / 100), @(1 - _percent / 100), @(1.0f)];

    maskLayer.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);

    maskLayer.anchorPoint = CGPointZero;

    self.layer.mask = maskLayer;

    [self updateMask];
}

- (void)updateMask
{
    UIScrollView *scrollView = self;

    CGColorRef outer = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
    CGColorRef inner = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;

    NSArray *colors = @[(__bridge id)outer, (__bridge id)inner, (__bridge id)inner, (__bridge id)outer];

    if(scrollView.contentOffset.y <= 0) // top
    {
        colors = @[(__bridge id)inner, (__bridge id)inner, (__bridge id)inner, (__bridge id)outer];
    }
    else if((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height) // bottom
    {
        colors = @[(__bridge id)outer, (__bridge id)inner, (__bridge id)inner, (__bridge id)inner];
    }

    ((CAGradientLayer *)scrollView.layer.mask).colors = colors;

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    scrollView.layer.mask.position = CGPointMake(0, scrollView.contentOffset.y);
    [CATransaction commit];
}

@end
fotios
  • 204
  • 4
  • 10
  • any idea of how i can implement something like this for just the bottom of the tableView? – SleepsOnNewspapers Feb 02 '15 at 22:11
  • In both this, and @Aviel Gross's solution, the colours and gradient values are re-initialized every time the mask is updated. Granted it's not a huge amount of work, but it's done needlessly and every little counts when it comes to saving battery power on-device. I would store these values in instance variables. – Chris Hatton May 02 '16 at 01:53