Search code examples
iosobjective-cimageperformancegpuimage

Fast blurring for UITableViewCell contentView Background


I have made a UIViewController which conforms to the UITableViewDataSource and UITableViewDelegate protocol and has a UITableView as it's subview.

I have set the backgroundView property of the table to be a UIImageView in order to display an image as the background of the table.

In order to have custom spacings between the cells I made the row height larger than I wanted and customised the cell's contentView to be the size I wanted, making it look like there is extra space (Following this SO answer).

I wanted to add a blur to the cell so that the background was blurred and I did this through Brad Larson's GPUImage framework. This works fine however, since I want the background blur to update as it scrolls, the scroll becomes very laggy.

My code is:

//Gets called from the -scrollViewDidScroll:(UIScrollView *)scrollView method
- (void)updateViewBG
{
    UIImage *superviewImage = [self snapshotOfSuperview:self.tableView];

    UIImage* newBG = [self applyTint:self.tintColour image:[filter imageByFilteringImage:superviewImage]];

    self.layer.contents = (id)newBG.CGImage;
    self.layer.contentsScale = newBG.scale;
}

//Code to create an image from the area behind the 'blurred cell'
- (UIImage *)snapshotOfSuperview:(UIView *)superview
{
    CGFloat scale = 0.5;
    if (([UIScreen mainScreen].scale > 1 || self.contentMode == UIViewContentModeScaleAspectFill)) {
        CGFloat blockSize = 12.0f/5;
        scale = blockSize/MAX(blockSize * 2, floor(self.blurRadius));
    }

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextTranslateCTM(context, -self.frame.origin.x, -self.frame.origin.y);
    NSArray *hiddenViews = [self prepareSuperviewForSnapshot:superview];
    [superview.layer renderInContext:context];
    [self restoreSuperviewAfterSnapshot:hiddenViews];
    UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshot;
}

-(UIImage*)applyTint:(UIColor*)colour image:(UIImage*)inImage{
    UIImage *newImage;
    if (colour) {
        UIGraphicsBeginImageContext(inImage.size);

        CGContextRef ctx = UIGraphicsGetCurrentContext();
        CGRect area = CGRectMake(0, 0, inImage.size.width, inImage.size.height);

        CGContextScaleCTM(ctx, 1, -1);
        CGContextTranslateCTM(ctx, 0, -area.size.height);
        CGContextSaveGState(ctx);
        CGContextClipToMask(ctx, area, inImage.CGImage);
        [[colour colorWithAlphaComponent:0.8] set];
        CGContextFillRect(ctx, area);
        CGContextRestoreGState(ctx);
        CGContextSetBlendMode(ctx, kCGBlendModeLighten);
        CGContextDrawImage(ctx, area, inImage.CGImage);
        newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    } else {
        newImage = inImage;
    }
    return newImage;
}

Now for the question:
Is there a better way to add the blur? Maybe so that the layer doesn't have to be rendered each movement? iOS7's control centre/notification centre seem to be able to do this without any lagging.

Maybe with the GPUImageUIElement class? If so, how do I use this? Another way I looked at was to create the blur on the background image initially and then crop just the areas I needed to use out, however I couldn't get this to work, since the images may or may not be the same size as the screen so the scaling was a problem (Using CGImageCreateWithImageInRect() and the rect being the cell's position on the table).

I also found out that I have to add the blur to the tableview itself with the frame being that of the cell, and the cell having a clear colour.

Thanks in advance

EDIT Upon request, here is the code for the image cropping I attempted before:

- (void)updateViewBG
{
    //self.bgImg is the pre-blurred image, -getContentViewFromCellFrame: is a convenience method to get just the content area from the whole cell (since the contentarea is smaller than the cell)
    UIImage* bg = [self cropImage:self.bgImg 
                           toRect:[LATableBlur getContentViewFromCellFrame:[self.tableView rectForRowAtIndexPath:self.cellIndexPath]]];
    bg = [self applyTint:self.tintColour image:bg];
    self.layer.contents = (id)bg.CGImage;
    self.layer.contentsScale = bg.scale;
}

- (UIImage*)cropImage:(UIImage*)image toRect:(CGRect)frame
{
    CGSize imgSize = [image size];
    double heightRatio = imgSize.height/self.tableView.frame.size.height;
    double widthRatio = imgSize.width/self.tableView.frame.size.width;

    UIImage* cropped = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(image.CGImage,
                                                                              CGRectMake(frame.origin.x*widthRatio,
                                                                                         frame.origin.y*heightRatio,
                                                                                         frame.size.width*widthRatio,
                                                                                         frame.size.height*heightRatio))];

    return cropped;
}

Solution

  • I managed to solve it with a solution I, at first, didn't think it would work.

    Generating several blurred images is certainly not the solution as it costs a lot. I used only one blurred image and cached it.

    So I subclassed UITableViewCell :

    @interface BlurredCell : UITableViewCell
    @end
    

    I implemented two class methods to access the cached images (blurred and normal ones)

    +(UIImage *)normalImage
    {
        static dispatch_once_t onceToken;
        static UIImage *_normalImage;
        dispatch_once(&onceToken, ^{
            _normalImage = [UIImage imageNamed:@"bg.png"];
        });
        return _normalImage;
    }
    

    I used REFrostedViewController's category on UIImage to generate the blurred image

    +(UIImage *)blurredImage
    {
        static dispatch_once_t onceToken;
        static UIImage *_blurredImage;
        dispatch_once(&onceToken, ^{
        _blurredImage = [[UIImage imageNamed:@"bg.png"] re_applyBlurWithRadius:BlurredCellBlurRadius
                                                                     tintColor:[UIColorcolorWithWhite:1.0f
                                                                                                alpha:0.4f]
                                                         saturationDeltaFactor:1.8f
                                                                     maskImage:nil];
        });
        return _blurredImage;
    }
    

    In order to have the effect of blurred frames inside the cell but still see the non blurred image on the sides, I used to scroll views.

    One with an image view with the normal image and the other one with an image view with the blurred image. I set the content size to be the size of the image and the contentOffset will be set through an interface.

    So the table view ends up with each cell holding the whole background image but cropping it at certain offset and still showing the entire image

    @implementation BlurredCell
    
    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
    {
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        if (self) {
            // Initialization code
            [self.contentView addSubview:self.normalScrollView];
            [self.contentView addSubview:self.blurredScrollView];
        }
        return self;
    }
    
    -(UIScrollView *)normalScrollView
    {
        if (!_normalScrollView) {
            _normalScrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
            _normalScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            _normalScrollView.scrollEnabled = NO;
            UIImageView *imageView =[[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
            imageView.contentMode = UIViewContentModeScaleToFill;
            imageView.image = [BlurredCell normalImage];
            _normalScrollView.contentSize = imageView.frame.size;
            [_normalScrollView addSubview:imageView];
        }
        return _normalScrollView;
    }
    
    -(UIScrollView *)blurredScrollView
    {
        if (!_blurredScrollView) {
            _blurredScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(BlurredCellPadding, BlurredCellPadding,
                                                                                self.bounds.size.width - 2.0f * BlurredCellPadding,
                                                                                self.bounds.size.height - 2.0f * BlurredCellPadding)];
            _blurredScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            _blurredScrollView.scrollEnabled = NO;
            _blurredScrollView.contentOffset = CGPointMake(BlurredCellPadding, BlurredCellPadding);
            UIImageView *imageView =[[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
            imageView.contentMode = UIViewContentModeScaleToFill;
            imageView.image = [BlurredCell blurredImage];
            _blurredScrollView.contentSize = imageView.frame.size;
            [_blurredScrollView addSubview:imageView];
        }
        return _blurredScrollView;
    }
    
    -(void)setBlurredContentOffset:(CGFloat)offset
    {
        self.normalScrollView.contentOffset = CGPointMake(self.normalScrollView.contentOffset.x, offset);
        self.blurredScrollView.contentOffset = CGPointMake(self.blurredScrollView.contentOffset.x, offset + BlurredCellPadding);
    }
    
    @end
    

    setBlurredContentOffset: should be called each time the table view's content offset changes.

    So in the table view delegate's implementation (the view controller) we do it in those two methods :

    // For the first rows
    -(void)tableView:(UITableView *)tableView willDisplayCell:(BlurredCell *)cell 
                                            forRowAtIndexPath:(NSIndexPath *)indexPath
    {
        [cell setBlurredContentOffset:cell.frame.origin.y];
    }
    
    // Each time the table view is scrolled
    -(void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
        for (BlurredCell *cell in [self.tableView visibleCells]) {
            [cell setBlurredContentOffset:cell.frame.origin.y - scrollView.contentOffset.y];
        }
    }
    

    Here is a complete working demo