17

I want to add a drop shadow using CALayer to a UI(Image)View. The following code works generally fine

previewImage.layer.shadowColor = [[UIColor blackColor] CGColor];
previewImage.layer.shadowOffset = CGSizeMake(1.0f, 1.0f);
previewImage.layer.shadowOpacity = 1.0f;
previewImage.layer.shadowRadius = 8.0f;

However, this only works if I create that view programmatically and add it as a subview to my main view. When that view is set up in InterfaceBuilder and defined as an IBOutlet UIImageView this does NOT work. No shadow appears. So what am I missing here?

Dennis
  • 2,223
  • 3
  • 27
  • 36
  • 1
    Quite randomly, I just found out the actual problem doesn't seem to be InterfaceBuilder, but the fact that I set my InterfaceBuilder view's clipsToBounds = YES. Having NO here is alright. So I guess then I'll have to wrap the view in a second view with clipsToBound = NO and the shadow. Is there another way? – Dennis Sep 20 '11 at 13:01
  • I had the same issue, but it was because "Clip Subviews" was checked for the view in Interface Builder. Unchecking that made the CALayer shadow visible. – dvs Jul 26 '12 at 19:49

5 Answers5

26

Add a file named UIView.swift in your project (or just paste this in any file) :

import UIKit

@IBDesignable extension UIView {

    /* The color of the shadow. Defaults to opaque black. Colors created
    * from patterns are currently NOT supported. Animatable. */
    @IBInspectable var shadowColor: UIColor? {
        set {
            layer.shadowColor = newValue!.CGColor
        }
        get {
            if let color = layer.shadowColor {
                return UIColor(CGColor:color)
            }
            else {
                return nil
            }
        }
    }

    /* The opacity of the shadow. Defaults to 0. Specifying a value outside the
    * [0,1] range will give undefined results. Animatable. */
    @IBInspectable var shadowOpacity: Float {
        set {
            layer.shadowOpacity = newValue
        }
        get {
            return layer.shadowOpacity
        }
    }

    /* The shadow offset. Defaults to (0, -3). Animatable. */
    @IBInspectable var shadowOffset: CGPoint {
        set {
            layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
        }
        get {
            return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
        }
    }

    /* The blur radius used to create the shadow. Defaults to 3. Animatable. */
    @IBInspectable var shadowRadius: CGFloat {
        set {
            layer.shadowRadius = newValue
        }
        get {
            return layer.shadowRadius
        }
    }
}

Then this will be available in Interface Builder for every view in the Utilities Panel > Attributes Inspector :

Utilities Panel

You can easily set the shadow now.

Notes:
- The shadow will only appear at runtime.
- clipsToBounds should be false (by default it is)

Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
17

I know this question has long, but recently i was in a similar situation so i decided to put my answer for those who are in a situation like this.

I wanted to be able to set the borderColor and shadowColor on an UIView through the Interface Builder, but the type of a layer’s borderColor property is CGColor (just like shadowColor) which is not one of the types allowed to be changed in the user-defined runtime attributes feature.

So i made an extension for CALayerand i added two properties called borderColorIB and shadowColorIB that are of type UIColor:

RuntimeAttributes.h

@import QuartzCore;

@interface CALayer (IBConfiguration)

@property(nonatomic, assign) UIColor* borderColorIB;
@property(nonatomic, assign) UIColor* shadowColorIB;

@end

RuntimeAttributes.m

#import <UIKit/UIKit.h>
#import "RuntimeAttributes.h"

@implementation CALayer (IBConfiguration)

-(void)setBorderColorIB:(UIColor*)color
{
    self.borderColor = color.CGColor;
}

-(UIColor*)borderColorIB
{
    return [UIColor colorWithCGColor:self.borderColor];
}

-(void)setShadowColorIB:(UIColor*)color
{
    self.shadowColor = color.CGColor;
}

-(UIColor*)shadowColorIB
{
    return [UIColor colorWithCGColor:self.shadowColor];
}

@end

Now i alredy be able to set this two properties through Interface Builder like this:

  1. In the 'user-defined runtime attributes' section (Identity inspector)
  2. Make sure the UIView is selected, and add the following runtime attributes:

    • layer.borderWidth, Number, 1
    • layer.borderColorIB, Color, someColor <- my custom property to set the borderColor
    • layer.shadowColorIB, Color, someColor <- my custom property to set the shadowColor
    • layer.shadowOpacity, Number, 0.8
    • layer.shadowOffset, size, {5,5}
    • layer.cornerRadius, Number, 5

Here is an image to show you how i did:

enter image description here

... and The result will be apparent during runtime, not in Xcode:

enter image description here

i hope this can help some people out there!

mauricioconde
  • 5,032
  • 3
  • 28
  • 24
3

Swift 5 Xcode 11.3.1 version of mauricioconde's answer. Note that without @IBInspectable it wouldn't work.

import UIKit
import QuartzCore

extension CALayer {

    @IBInspectable
    var shadowUIColor: UIColor? {
        get { shadowColor != nil ? UIColor(cgColor: shadowColor!) : nil }
        set { shadowColor = newValue?.cgColor }
    }
}

enter image description here

HammerSlavik
  • 161
  • 1
  • 5
3

I'm not sure what the issue is - ensure your UIImageView's clipsToBounds property is set to NO. You can do this in viewDidLoad after loading from the nib file by referencing your IBOutlet. You shouldn't need to wrap it in another view.

Edit

In light of you needing your image to be scaled using aspect fill, you can use the contentsRect property of the UIImageView's underlying layer to 'simulate' the effect of the contents clipping. contentsRect is rectangle in the unit coordinate space of the layer's content (in this case your image) that defines a sub-rectangle of the contents that should be drawn.

With a little bit of maths, we can find this rectangle by comparing the image view size with the image size (accounting for the aspect fill scaling):

CGSize imageViewSize = previewImage.size;
CGSize imageSize = previewImage.image.size;

// Find the scaling required for the image to fit the image view (as for aspect fill).
CGFloat imageWidthScale = fabsf(imageViewSize.width / imageSize.width);
CGFloat imageHeightScale = fabsf(imageViewSize.height / imageSize.height);
CGFloat imageScale = (imageWidthScale > imageHeightScale) ? imageWidthScale : imageHeightScale;

// Determine the new image size, after scaling.
CGSize scaledImageSize = CGSizeApplyAffineTransform(imageSize, CGAffineTransformMakeScale(imageScale, imageScale));

// Set the layer's contentsRect property in order to 'clip' the image to the image view's bounds.
previewImage.layer.contentsRect = CGRectMake(((scaledImageSize.width - imageViewSize.width) / 2.0f) / scaledImageSize.width,
                                             ((scaledImageSize.height - imageViewSize.height) / 2.0f) / scaledImageSize.height,
                                             imageViewSize.width / scaledImageSize.width,
                                             imageViewSize.height / scaledImageSize.height);

Doing this, you can leave clipsToBounds set to NO for your image view, but the image will still appear clipped. If you need to change the image view size at all, it might be convenient to wrap this code up into a method that takes a UIImageView as a parameter.

I hope this helps.

Stuart
  • 36,683
  • 19
  • 101
  • 139
  • I agree. No wrapping necessary. Just don't clipToBounds - as the shadow is outside of the bounds. – bandejapaisa Sep 20 '11 at 15:01
  • The reason I need clipToBounds is, that image is too big to fit the view and I need to use "Aspect Fill" to make it look good. I could, of course, crop the image itself by drawing it entirely new and then getting a fitting one through UIGraphicsGetImageFromCurrentImageContext(). But wouldn't that be a lot overhead? – Dennis Sep 20 '11 at 16:11
  • @Dennis: If the image is static and will only ever be used at that size, then my first suggestion would be to recreate the image in your image editing software first, and then use it in your project's image view without the need to alter its geometry. If this is not an option, however, then see my edit above. – Stuart Sep 20 '11 at 17:48
1

UIView shadow with corner radius + interface builder - swift4

    extension UIView {

        @IBInspectable
        var cornerRadius: CGFloat {
            get {
                return layer.cornerRadius
            }
            set {
                layer.cornerRadius = newValue
                if shadowOpacity > 0.0 {
                    layer.masksToBounds = false
                }
                else {
                    layer.masksToBounds = true
                }
            }
        }
      @IBInspectable
        var borderWidth: CGFloat {
            get {
                return layer.borderWidth
            }
            set {
                layer.borderWidth = newValue
            }
        }
    @IBInspectable
        var borderColor: UIColor? {
            get {
                if let color = layer.borderColor {
                    return UIColor(cgColor: color)
                }
                return nil
            }
            set {
                if let color = newValue {
                    layer.borderColor = color.cgColor
                } else {
                    layer.borderColor = nil
                }
            }
      @IBInspectable var shadowColor: UIColor? {
            set {
                layer.shadowColor = newValue!.cgColor
            }
            get {
                if let color = layer.shadowColor {
                    return UIColor(cgColor: color)
                }
                else {
                    return nil
                }
            }
        }
       @IBInspectable var shadowOpacity: Float {
            set {
                layer.shadowOpacity = newValue
            }
            get {
                return layer.shadowOpacity
            }
        }
     @IBInspectable var shadowOffset: CGPoint {
            set {
                layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
            }
            get {
                return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
            }
        }
     @IBInspectable var shadowRadius: CGFloat {
            set {
                layer.shadowRadius = newValue
            }
            get {
                return layer.shadowRadius
            }
        }
}
midhun p
  • 1,987
  • 18
  • 24