iosobjective-cuiviewcalayeruivisualeffectview

How do I add both shadow and rounded corners to a UIVisualEffectView?


I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.

It works, but not flawlessly. The shadow darkens the effect of the blur. Is there a way to perfect it?

.h

@property (strong) UIVisualEffectView *containerView;

@property (strong) UIView *containerShadowView;

.m

- (instancetype)init {
    
    if (self = [super init]) {

        self.containerShadowView = [[UIView alloc] init];
        
        self.containerShadowView.layer.masksToBounds = NO;
        
        self.containerShadowView.layer.shadowRadius = 80.0;
        
        self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
        
        self.containerShadowView.layer.shadowOffset = CGSizeZero;
        
        self.containerShadowView.layer.shadowOpacity = 0.25;
        
        [self addSubview:self.containerShadowView];
        
        self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
        
        self.containerView.clipsToBounds = YES;
        
        self.containerView.layer.cornerRadius = 20.0;
        
        [self addSubview:self.containerView];
    }
    
    return self;
}

- (void)setFrame:(CGRect)frame {
    
    [super setFrame:frame];
    
    // Random container frame for testing...

    self.containerView.frame =
    CGRectMake(20.0,
               200.0,
               380,
               480);

    self.containerShadowView.frame = self.containerView.frame;
    
    self.containerShadowView.layer.shadowPath =
    [[UIBezierPath bezierPathWithRoundedRect:self.containerShadowView.bounds cornerRadius:self.containerView.layer.cornerRadius] CGPath];
}

enter image description here


Solution

  • You can do this by masking your containerShadowView with a "cutout" that matches the containerView effect view.

    So, this is how it looks - I centered the 380x480 view, and used 0.9 for the .shadowOpacity to emphasize the differences.

    Your original on the left, masked version on the right:

    Kinda difficult to tell what's really going on, since that could be an opaque layer, we'll add a label behind it:

    and, to clarify what we're doing, let's look at it with the containerView effect view hidden:

    Here's the source code I used for that - each tap anywhere will cycle through the 8 different layouts:

    #import <UIKit/UIKit.h>
    
    @interface OrigShadowView : UIView
    
    @property (strong) UIVisualEffectView *containerView;
    @property (strong) UIView *containerShadowView;
    
    @end
    
    @implementation OrigShadowView
    
    - (instancetype)init {
        
        if (self = [super init]) {
            
            self.containerShadowView = [[UIView alloc] init];
            
            self.containerShadowView.layer.masksToBounds = NO;
            
            self.containerShadowView.layer.shadowRadius = 80.0;
            
            self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
            
            self.containerShadowView.layer.shadowOffset = CGSizeZero;
            
            self.containerShadowView.layer.shadowOpacity = 0.9;
            
            [self addSubview:self.containerShadowView];
            
            self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
            
            self.containerView.clipsToBounds = YES;
            
            self.containerView.layer.cornerRadius = 20.0;
            
            [self addSubview:self.containerView];
            
        }
        
        return self;
    }
    
    - (void)setFrame:(CGRect)frame {
        [super setFrame:frame];
        
        // let's center a 380 x 480 rectangle in self
        CGFloat w = 380.0;
        CGFloat h = 480.0;
        
        CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
    
        self.containerView.frame = vRect;
        
        self.containerShadowView.frame = self.containerView.frame;
        
        // change origin to 0,0 because the following will be relative to the subviews
        vRect.origin = CGPointZero;
        
        self.containerShadowView.layer.shadowPath =
        [[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
        
    }
    
    @end
    
    @interface MaskShadowView : UIView
    
    @property (strong) UIVisualEffectView *containerView;
    @property (strong) UIView *containerShadowView;
    
    @end
    
    @implementation MaskShadowView
    
    - (instancetype)init {
        
        if (self = [super init]) {
            
            self.containerShadowView = [[UIView alloc] init];
            
            self.containerShadowView.layer.masksToBounds = NO;
            
            self.containerShadowView.layer.shadowRadius = 80.0;
            
            self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
            
            self.containerShadowView.layer.shadowOffset = CGSizeZero;
            
            self.containerShadowView.layer.shadowOpacity = 0.9;
            
            [self addSubview:self.containerShadowView];
            
            self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
            
            self.containerView.clipsToBounds = YES;
            
            self.containerView.layer.cornerRadius = 20.0;
            
            [self addSubview:self.containerView];
            
        }
        
        return self;
    }
    
    - (void)setFrame:(CGRect)frame {
        [super setFrame:frame];
    
        // let's center a 380 x 480 rectangle in self
        CGFloat w = 380.0;
        CGFloat h = 480.0;
        
        CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
    
        self.containerView.frame = vRect;
        
        self.containerShadowView.frame = self.containerView.frame;
    
        // change origin to 0,0 because the following will be relative to the subviews
        vRect.origin = CGPointZero;
    
        self.containerShadowView.layer.shadowPath =
        [[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
    
        UIBezierPath *bigBez;
        UIBezierPath *clipBez;
    
        // we need a rectangle that will encompass the shadow radius
        //  double the shadowRadius is probably sufficient, but since it won't be seen
        //  and won't affect anything else, we'll make it 4x
        CGRect expandedRect = CGRectInset(vRect, -self.containerShadowView.layer.shadowRadius * 4.0, -self.containerShadowView.layer.shadowRadius * 4.0);
    
        bigBez = [UIBezierPath bezierPathWithRect:expandedRect];
        
        // we want to "clip out" a rounded rect in the center
        //  which will be the same size as the visual effect view
        clipBez = [UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius];
    
        [bigBez appendPath:clipBez];
        bigBez.usesEvenOddFillRule = YES;
    
        CAShapeLayer *maskLayer = [CAShapeLayer new];
        maskLayer.fillRule = kCAFillRuleEvenOdd;
        maskLayer.fillColor = UIColor.whiteColor.CGColor;
        maskLayer.path = bigBez.CGPath;
        self.containerShadowView.layer.mask = maskLayer;
    
    }
    
    @end
    
    @interface BlurTestViewController : UIViewController
    {
        OrigShadowView *origView;
        MaskShadowView *newView;
        UILabel *bkgLabel;
        
        // so we can step through on taps to see the results
        NSInteger step;
        UILabel *infoLabel;
    }
    @end
    
    @implementation BlurTestViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        bkgLabel = [UILabel new];
        bkgLabel.textColor = UIColor.blueColor;
        bkgLabel.font = [UIFont systemFontOfSize:48.0 weight:UIFontWeightBlack];
        bkgLabel.textAlignment = NSTextAlignmentCenter;
        bkgLabel.numberOfLines = 0;
        bkgLabel.text = @"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.";
        bkgLabel.text = @"I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.";
        origView = [OrigShadowView new];
        newView = [MaskShadowView new];
    
        [self.view addSubview:bkgLabel];
        [self.view addSubview:origView];
        [self.view addSubview:newView];
        
        infoLabel = [UILabel new];
        infoLabel.font = [UIFont systemFontOfSize:20.0 weight:UIFontWeightBold];
        infoLabel.textAlignment = NSTextAlignmentCenter;
        [self.view addSubview:infoLabel];
        
        step = 0;
    
    }
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
    
        // let's inset the "shadow blur" views 40-points
        CGRect r = CGRectInset(self.view.frame, 40.0, 40.0);
        
        origView.frame = r;
        newView.frame = r;
    
        // let's put the background label midway down the screen
        r.origin.y += r.size.height * 0.5;
        r.size.height *= 0.5;
        bkgLabel.frame = r;
        
        // put the info label near the top
        infoLabel.frame = CGRectMake(40.0, 80.0, r.size.width, 40.0);
    
        [self nextStep];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self nextStep];
    }
    
    - (void)nextStep {
        bkgLabel.hidden = YES;
        origView.hidden = YES;
        newView.hidden = YES;
        origView.containerView.hidden = NO;
        newView.containerView.hidden = NO;
    
        step++;
        switch (step) {
            case 1:
                origView.hidden = NO;
                infoLabel.text = @"1: Original View";
                break;
    
            case 2:
                newView.hidden = NO;
                infoLabel.text = @"2: Masked View";
                break;
                
            case 3:
                bkgLabel.hidden = NO;
                origView.hidden = NO;
                infoLabel.text = @"3: Original View";
                break;
                
            case 4:
                bkgLabel.hidden = NO;
                newView.hidden = NO;
                infoLabel.text = @"4: Masked View";
                break;
                
            case 5:
                origView.hidden = NO;
                origView.containerView.hidden = YES;
                infoLabel.text = @"5: Original View - effect view hidden";
                break;
                
            case 6:
                newView.hidden = NO;
                newView.containerView.hidden = YES;
                infoLabel.text = @"6: Masked View - effect view hidden";
                break;
                
            case 7:
                bkgLabel.hidden = NO;
                origView.hidden = NO;
                origView.containerView.hidden = YES;
                infoLabel.text = @"7: Original View - effect view hidden";
                break;
                
            default:
                bkgLabel.hidden = NO;
                newView.hidden = NO;
                newView.containerView.hidden = YES;
                infoLabel.text = @"8: Masked View - effect view hidden";
                step = 0;
                break;
        }
    }
    @end