92

I have the following CALayer:

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(8, 57, 296, 30);
gradient.cornerRadius = 3.0f;
gradient.colors = [NSArray arrayWithObjects:(id)[RGB(130, 0, 140) CGColor], (id)[RGB(108, 0, 120) CGColor], nil];
[self.layer insertSublayer:gradient atIndex:0];

I'd like to add an inner shadow effect to it, but I am not quite sure how to do this. I suppose I would be required to draw in drawRect, however this would add the layer on top of other UIView objects, since it's supposed to be a bar behind some buttons, so I am at a loss as to what to do?

I could add another layer, but again, not sure how to achieve the inner shadow effect (like this:

enter image description here

Help appreciated...

runmad
  • 14,846
  • 9
  • 99
  • 140

17 Answers17

110

For anyone else wondering how to draw an inner shadow using Core Graphics as per Costique's suggestion, then this is how: (on iOS adjust as needed)

In your drawRect: method...

CGRect bounds = [self bounds];
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat radius = 0.5f * CGRectGetHeight(bounds);


// Create the "visible" path, which will be the shape that gets the inner shadow
// In this case it's just a rounded rect, but could be as complex as your want
CGMutablePathRef visiblePath = CGPathCreateMutable();
CGRect innerRect = CGRectInset(bounds, radius, radius);
CGPathMoveToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x + innerRect.size.width, bounds.origin.y);
CGPathAddArcToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, bounds.origin.y, bounds.origin.x + bounds.size.width, innerRect.origin.y, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, innerRect.origin.y + innerRect.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x + bounds.size.width, bounds.origin.y + bounds.size.height, innerRect.origin.x + innerRect.size.width, bounds.origin.y + bounds.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y + bounds.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y + bounds.size.height, bounds.origin.x, innerRect.origin.y + innerRect.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x, innerRect.origin.y);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y, innerRect.origin.x, bounds.origin.y, radius);
CGPathCloseSubpath(visiblePath);

// Fill this path
UIColor *aColor = [UIColor redColor];
[aColor setFill];
CGContextAddPath(context, visiblePath);
CGContextFillPath(context);


// Now create a larger rectangle, which we're going to subtract the visible path from
// and apply a shadow
CGMutablePathRef path = CGPathCreateMutable();
//(when drawing the shadow for a path whichs bounding box is not known pass "CGPathGetPathBoundingBox(visiblePath)" instead of "bounds" in the following line:)
//-42 cuould just be any offset > 0
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));
    
// Add the visible path (so that it gets subtracted for the shadow)
CGPathAddPath(path, NULL, visiblePath);
CGPathCloseSubpath(path);

// Add the visible paths as the clipping path to the context
CGContextAddPath(context, visiblePath); 
CGContextClip(context);         


// Now setup the shadow properties on the context
aColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, CGSizeMake(0.0f, 1.0f), 3.0f, [aColor CGColor]);   

// Now fill the rectangle, so the shadow gets drawn
[aColor setFill];   
CGContextSaveGState(context);   
CGContextAddPath(context, path);
CGContextEOFillPath(context);

// Release the paths
CGPathRelease(path);    
CGPathRelease(visiblePath);

So, essentially there are the following steps:

  1. Create your path
  2. Set the fill color you want, add this path to the context, and fill the context
  3. Now create a larger rectangle that can bound the visible path. Before closing this path, add the visible path. Then close the path, so that you create a shape with the visible path subtracted from it. You might want to investigate the fill methods (non-zero winding of even/odd) depending on how you created these paths. In essence, to get the subpaths to "subtract" when you add them together, you need to draw them (or rather construct them) in opposite directions, one clockwise and the other anti-clockwise.
  4. Then you need to set your visible path as the clipping path on the context, so that you don't draw anything outside it to the screen.
  5. Then setup up the shadow on the context, which includes the offset, blur and color.
  6. Then fill the big shape with the hole in it. The color doesn't matter, because if you've done everything right, you won't see this color, just the shadow.
Alex Zavatone
  • 4,106
  • 36
  • 54
Daniel Thorpe
  • 3,911
  • 2
  • 28
  • 28
  • Thanks, but is it possible to adjust the radius? It's currently based on the bounds, but I'd like to based on a set radius instead (like 5.0f). With the above code, it's rounded way too much. – runmad Oct 20 '11 at 23:49
  • 2
    @runmad Well, you can create any sort of visible CGPath that you want, the example used here is just that, an example, chosen for brevity. If you'd like to create a rounded rect, then you can just do something like: CGPath visiblePath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath Hope that helps. – Daniel Thorpe Oct 21 '11 at 09:49
  • 4
    @DanielThorpe: +1 for the nice answer. I fixed the rounded rect path code (yours broke when changing the radius) and simplified the outer rect path code. Hope you don't mind. – Regexident Feb 19 '12 at 17:27
  • How can I properly setup the inner shadow from 4 directions, not just 2? – Protocole Jun 03 '13 at 04:53
  • @Protocole you can set the offset to {0,0}, but use a shadow radius of say, 4.f. – Daniel Thorpe Jun 03 '13 at 22:07
  • sorry if I'm being ignorant, I just see a red circle on a black background. – Dani Pralea Oct 23 '13 at 20:09
  • @danipralea If you see a red circle, it means that you haven't constructed your CGPath correctly. You need the circle to be subtracted from a bounding box - and to achieve this you need to construct one going clockwise and the other anticlockwise. – Daniel Thorpe Oct 24 '13 at 12:44
  • Weird when i use this for a custom UIView on top of another UIImageView the path disappears but if I hide the Image I get the path. – cynistersix May 21 '14 at 23:39
  • It seems that the shadow has to be more than 1.f if I offset it to (0.,0.) and then it starts to draw it... Not sure I'm a fan of this solution just yet though it is quite clever. – cynistersix May 21 '14 at 23:51
  • @DanielThorpe can we set a shadow path using this approach? When I tried setting a shadow path, the shadow is faded outwards the path instead of inwards. I need shadow path to have the shadow spread as suggested in here https://stackoverflow.com/a/48489506/1269509 – Thanh Pham Oct 22 '18 at 08:40
49

I know I'm late to this party, but this would have helped me to find early in my travels...

To give credit where credit's due, this is essentially a modification of Daniel Thorpe's elaboration on Costique's solution of subtracting a smaller region from a larger region. This version is for those using layer composition instead of overriding -drawRect:

The CAShapeLayer class can be used to achieve the same effect:

CAShapeLayer *shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:[self bounds]];

// Standard shadow stuff
[shadowLayer setShadowColor:[[UIColor colorWithWhite:0 alpha:1] CGColor]];
[shadowLayer setShadowOffset:CGSizeMake(0.0f, 0.0f)];
[shadowLayer setShadowOpacity:1.0f];
[shadowLayer setShadowRadius:5];

// Causes the inner region in this example to NOT be filled.
[shadowLayer setFillRule:kCAFillRuleEvenOdd];

// Create the larger rectangle path.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));

// Add the inner path so it's subtracted from the outer path.
// someInnerPath could be a simple bounds rect, or maybe
// a rounded one for some extra fanciness.
CGPathAddPath(path, NULL, someInnerPath);
CGPathCloseSubpath(path);

[shadowLayer setPath:path];
CGPathRelease(path);

[[self layer] addSublayer:shadowLayer];

At this point, if your parent layer isn't masking to its bounds, you'll see the extra area of the mask layer around the edges of the layer. This will be 42 pixels of black if you just copied the example directly. To get rid of it, you can simply use another CAShapeLayer with the same path and set it as the mask of the shadow layer:

CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setPath:someInnerPath];
[shadowLayer setMask:maskLayer];

I haven't benchmarked this myself, but I suspect that using this approach in conjunction with rasterization is more performant than overriding -drawRect:.

Alex Zavatone
  • 4,106
  • 36
  • 54
Matt Wilding
  • 20,115
  • 3
  • 67
  • 95
  • 3
    someInnerPath? Could you explain that a little more please. – Moe Jul 21 '12 at 04:28
  • 4
    @Moe It can be any arbitrary CGPath you want. `[[UIBezierPath pathWithRect:[shadowLayer bounds]] CGPath]` being the simplest choice. – Matt Wilding Jul 21 '12 at 15:11
  • I'm getting a black (outer) rectangle for the shadowLayer.path that correctly draws the inner shadow. How can I get rid of it (the black outer rectangle)? Looks like you can only set the fillColor inside a context & you do not use one. – Olivier Aug 15 '12 at 12:10
  • @Olivier I can't quite picture your problem, but it's probably a side effect of some other code somewhere. Do you mean you're getting and outer _and_ inner shadow? – Matt Wilding Aug 15 '12 at 15:05
  • @MattWilding Here is my/your code (https://gist.github.com/3363043), and the result (http://goo.gl/EuvL3), this code correctly append the white inner shadow, but I get an unwanted black opaque rectangle (the shadowLayer path - larger by 42.0f). – Olivier Aug 15 '12 at 19:51
  • @Olivier one immediate difference is that you're setting a red background color on the shadow layer... it only makes sense if the shadow layer has a clear (default) background color. – Matt Wilding Aug 15 '12 at 20:12
  • @MattWilding Unfortunately I have the issue without `shadowLayer setBackgroundColor:UIColor.redColor.CGColor`& even with `shadowLayer setBackgroundColor:UIColor.clearColor.CGColor` (red was just a test). – Olivier Aug 15 '12 at 20:24
  • @Olivier, your image is also from a OSX app. I've never run the code on OSX (though I would be surprised if it didn't work). The fact that the color is black, despite a white shadow is odd. Is that image the direct result of displaying the layers, or are you drawing them into a CGContext first or something? – Matt Wilding Aug 15 '12 at 20:33
  • @MattWilding it is applied to a UIButton (from Chameleon) layer directly, without using CGContext. Also I tried `[shadowLayer setFillColor:UIColor.clearColor.CGColor];` but it also removes the inner shadow, same thing for `[shadowLayer setMasksToBounds:YES];`. – Olivier Aug 15 '12 at 20:36
  • Found it : [self.layer setMasksToBounds:YES]; does the trick! – Olivier Aug 15 '12 at 20:37
  • @Olivier, Oh yeah, my code was using an explicit mask layer, which I neglected to post. I've added that info to the answer. Thanks for noticing that. The benefit of using an explicit mask is that it will work for arbitrarily complicated shadow paths, not just rectangular ones. – Matt Wilding Aug 15 '12 at 21:07
  • 12
    This works very nice! I uploaded to github with some additions. Have a try:) https://github.com/inamiy/YIInnerShadowView – inamiy Oct 14 '12 at 15:45
  • @inamiy, I have used your code to instead subclass UITextView. Very small change, but I am happy with the fact that I can use it with different types of views! Splendid work – pnizzle Dec 16 '12 at 23:29
  • @MattWilding @inamiy I have added three `YIIInnerShadowView` objects to a scroll view and scrolling is not as smooth as it should be. I'll try to discover what can be improved – elitalon Apr 06 '13 at 21:36
  • I can vouch for @elitalon. When I have 4 (or more, of course) of these shadows on table view cells, scrolling is noticeably jittery and not smooth. I am not experienced enough in CoreGraphics/Quartz to find where the performance hit is. – James Linnell Sep 07 '13 at 04:14
  • @Jman012 You can improve performance by using rasterization (which creates a cached bitmap of a CALayer). `layer.shouldRasterize = YES; layer.rasterizationScale = [UIScreen mainScreen].scale;` – Andreas Ley Oct 02 '13 at 15:50
  • @MattWilding Is there any way so i can set inner shadow based on predefine sided. Like only top or top/bottom or top/right etc .. – Mitesh Dobareeya Mar 27 '18 at 10:32
35

A simplified version using just a CALayer, in Swift:

import UIKit

final class FrameView : UIView {
    init() {
        super.init(frame: CGRect.zero)
        backgroundColor = UIColor.white
    }

    @available(*, unavailable)
    required init?(coder decoder: NSCoder) { fatalError("unavailable") }

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

    private func addInnerShadow() {
        let innerShadow = CALayer()
        innerShadow.frame = bounds
        // Shadow path (1pt ring around bounds)
        let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -1, dy: -1))
        let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
        path.append(cutout)
        innerShadow.shadowPath = path.cgPath
        innerShadow.masksToBounds = true
        // Shadow properties
        innerShadow.shadowColor = UIColor(white: 0, alpha: 1).cgColor // UIColor(red: 0.71, green: 0.77, blue: 0.81, alpha: 1.0).cgColor
        innerShadow.shadowOffset = CGSize.zero
        innerShadow.shadowOpacity = 1
        innerShadow.shadowRadius = 3
        // Add
        layer.addSublayer(innerShadow)
    }
}

Note that the innerShadow layer should not have an opaque background color as this will be rendered in front of the shadow.

Patrick Pijnappel
  • 7,317
  • 3
  • 39
  • 39
35

It is possible to draw an inner shadow with Core Graphics by making a large rectangle path outside the bounds, subtracting a bounds-sized rectangle path and filling the resulting path with a "normal" shadow on.

However, since you need to combine it with a gradient layer, I think an easier solution is to create a 9-part transparent PNG image of the inner shadow and stretch it to the right size. The 9-part shadow image would look like this (its size is 21x21 pixels):

alt text

CALayer *innerShadowLayer = [CALayer layer];
innerShadowLayer.contents = (id)[UIImage imageNamed: @"innershadow.png"].CGImage;
innerShadowLayer.contentsCenter = CGRectMake(10.0f/21.0f, 10.0f/21.0f, 1.0f/21.0f, 1.0f/21.0f);

Then set innerShadowLayer's frame and it should stretch the shadow properly.

Costique
  • 23,712
  • 4
  • 76
  • 79
  • Yeah, I suppose you're right. Just wanted the layer to be as flat as possible. I could create the image in Photoshop with the inner shadow and gradient look, I just have issues with the colours matching up 100% on the device when using an image. – runmad Dec 13 '10 at 17:45
  • Yep, that's an issue with all gradients and shadows, I just cannot reproduce these Photoshop effects 1:1 on iOS, hard as I try. – Costique Dec 13 '10 at 17:51
24

Bit of a round-about way, but it avoids having to use images (read: easy to change colors, shadow radius, etc.) and it's only a few lines of code.

  1. Add a UIImageView as the first subview of the UIView you want the dropshadow on. I use IB, but you can do the same programmatically.

  2. Assuming the reference to the UIImageView is 'innerShadow'

`

[[innerShadow layer] setMasksToBounds:YES];
[[innerShadow layer] setCornerRadius:12.0f];        
[[innerShadow layer] setBorderColor:[UIColorFromRGB(180, 180, 180) CGColor]];
[[innerShadow layer] setBorderWidth:1.0f];
[[innerShadow layer] setShadowColor:[UIColorFromRGB(0, 0, 0) CGColor]];
[[innerShadow layer] setShadowOffset:CGSizeMake(0, 0)];
[[innerShadow layer] setShadowOpacity:1];
[[innerShadow layer] setShadowRadius:2.0];

Caveat: You have to have a border, or else the shadow doesn't show up. [UIColor clearColor] doesn't work. In the example I use a different color, but you can mess with it to get it to have the same color as the beginning of the shadow. :)

See bbrame's comment below about the UIColorFromRGB macro.

jinglesthula
  • 4,446
  • 4
  • 45
  • 79
  • I left it out but assume you'd do this as part of adding the imageview - make sure to set the frame to the same rect as the parent UIView. If you're using IB, set struts and springs right to have the shadow size with the view if you'll be changing the parent view's frame. In code there should be a resize mask you can OR to do the same, AFAIK. – jinglesthula Jun 10 '11 at 17:49
  • This is the easiest way now, but be aware that the CALayer shadow methods are only available in iOS 3.2 and later. I support 3.1, so I surround setting these attributes in an if ([layer respondsToSelector:@selector(setShadowColor:)]) { – DougW Jul 12 '11 at 19:54
  • This doesn't seem to work for me. At least on xcode 4.2 and ios simulator 4.3. To make the shadow appear I have to add a background color... at which point the dropshadow appears only outside. – Andrea Jul 17 '11 at 15:36
  • @Andrea - bear in mind the caveat I mentioned above. I think a background color or a border might have the same effect of 'giving it something to add the shadow to'. As for it appearing outside, if the UIImageView isn't a subview of the one you want the inner shadow on that might be it - I'd have to look at your code to see. – jinglesthula Jul 18 '11 at 22:57
  • Just to rectify my previous statement... the code actually works... I was missing something but unfortunately I can't remember it right now. :) So... thanks for sharing this code snippet. – Andrea Aug 19 '11 at 14:07
  • It's worth noting that where I was using this the blinking cursor that a UITextView provides causes a blinking black rectangle if you put one inside/above a UIView that uses this technique for inner shadowing. I resolved it by giving the UITextView a background color other than clearColor. Not ideal, so I'd go with Daniel's suggestion above if you're looking for color flexibility (image-free) and you find this is an issue. – jinglesthula Aug 22 '11 at 23:45
  • Note: need to #import and use the macro defined here http://stackoverflow.com/questions/1243201/is-macro-better-than-uicolor-for-setting-rgb-color – bbrame Apr 03 '12 at 13:32
  • @bbrame ah - thanks. I'd copy/pasted from a project where I'd used that bit. Good catch. – jinglesthula Apr 10 '12 at 16:31
  • doesn't seem to have any effect in my setting – Pascalius Aug 08 '12 at 09:40
  • Simplest answer by far, and it does the trick as well as any other. One note: the border width should be set to greater than 1.0 if you want a darker shadow. The shadow is very dim even at max opacity if you leave the hairline border. – puzzl Sep 19 '14 at 21:36
  • @jinglesthula Is there any way so i can set shadow with side specific. Like only in top or top/left or top/right etc.. – Mitesh Dobareeya Mar 27 '18 at 10:10
17

Better late than never...

Here's another approach, probably not any better than those already posted, but it's nice & simple -

-(void)drawInnerShadowOnView:(UIView *)view
{
    UIImageView *innerShadowView = [[UIImageView alloc] initWithFrame:view.bounds];

    innerShadowView.contentMode = UIViewContentModeScaleToFill;
    innerShadowView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [view addSubview:innerShadowView];

    [innerShadowView.layer setMasksToBounds:YES];

    [innerShadowView.layer setBorderColor:[UIColor lightGrayColor].CGColor];
    [innerShadowView.layer setShadowColor:[UIColor blackColor].CGColor];
    [innerShadowView.layer setBorderWidth:1.0f];

    [innerShadowView.layer setShadowOffset:CGSizeMake(0, 0)];
    [innerShadowView.layer setShadowOpacity:1.0];

    // this is the inner shadow thickness
    [innerShadowView.layer setShadowRadius:1.5];
}
SomaMan
  • 4,127
  • 1
  • 34
  • 45
8

Instead of drawing inner shadow by drawRect or add UIView to the View. You may directly Add CALayer to the border, for example: if I want inner shadow effect on UIView V's bottom.

innerShadowOwnerLayer = [[CALayer alloc]init];
innerShadowOwnerLayer.frame = CGRectMake(0, V.frame.size.height+2, V.frame.size.width, 2);
innerShadowOwnerLayer.backgroundColor = [UIColor whiteColor].CGColor;

innerShadowOwnerLayer.shadowColor = [UIColor blackColor].CGColor;
innerShadowOwnerLayer.shadowOffset = CGSizeMake(0, 0);
innerShadowOwnerLayer.shadowRadius = 10.0;
innerShadowOwnerLayer.shadowOpacity = 0.7;

[V.layer addSubLayer:innerShadowOwnerLayer];

This add a bottom inner shadow for target UIView

Jerry Zhang
  • 96
  • 1
  • 3
6

Here is a version of swift, change startPoint and endPoint to make it on each side.

        let layer = CAGradientLayer()
        layer.startPoint    = CGPointMake(0.5, 0.0);
        layer.endPoint      = CGPointMake(0.5, 1.0);
        layer.colors        = [UIColor(white: 0.1, alpha: 1.0).CGColor, UIColor(white: 0.1, alpha: 0.5).CGColor, UIColor.clearColor().CGColor]
        layer.locations     = [0.05, 0.2, 1.0 ]
        layer.frame         = CGRectMake(0, 0, self.view.frame.width, 60)
        self.view.layer.insertSublayer(layer, atIndex: 0)
William Hu
  • 15,423
  • 11
  • 100
  • 121
5

This is your solution, which I've exported from PaintCode :

-(void) drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    //// Shadow Declarations
    UIColor* shadow = UIColor.whiteColor;
    CGSize shadowOffset = CGSizeMake(0, 0);
    CGFloat shadowBlurRadius = 10;

    //// Rectangle Drawing
    UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: self.bounds];
    [[UIColor blackColor] setFill];
    [rectanglePath fill];

    ////// Rectangle Inner Shadow
    CGContextSaveGState(context);
    UIRectClip(rectanglePath.bounds);
    CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);

    CGContextSetAlpha(context, CGColorGetAlpha([shadow CGColor]));
    CGContextBeginTransparencyLayer(context, NULL);
    {
        UIColor* opaqueShadow = [shadow colorWithAlphaComponent: 1];
        CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, [opaqueShadow CGColor]);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextBeginTransparencyLayer(context, NULL);

        [opaqueShadow setFill];
        [rectanglePath fill];

        CGContextEndTransparencyLayer(context);
    }
    CGContextEndTransparencyLayer(context);
    CGContextRestoreGState(context);
}
4

Here is my solution in Swift 4.2. Would you want to try?

final class ACInnerShadowLayer : CAShapeLayer {

  var innerShadowColor: CGColor? = UIColor.black.cgColor {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOffset: CGSize = .zero {
    didSet { setNeedsDisplay() }
  }

  var innerShadowRadius: CGFloat = 8 {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOpacity: Float = 1 {
    didSet { setNeedsDisplay() }
  }

  override init() {
    super.init()

    masksToBounds = true
    contentsScale = UIScreen.main.scale

    setNeedsDisplay()
  }

  override init(layer: Any) {
      if let layer = layer as? InnerShadowLayer {
          innerShadowColor = layer.innerShadowColor
          innerShadowOffset = layer.innerShadowOffset
          innerShadowRadius = layer.innerShadowRadius
          innerShadowOpacity = layer.innerShadowOpacity
      }
      super.init(layer: layer)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func draw(in ctx: CGContext) {
    ctx.setAllowsAntialiasing(true)
    ctx.setShouldAntialias(true)
    ctx.interpolationQuality = .high

    let colorspace = CGColorSpaceCreateDeviceRGB()

    var rect = bounds
    var radius = cornerRadius

    if borderWidth != 0 {
      rect = rect.insetBy(dx: borderWidth, dy: borderWidth)
      radius -= borderWidth
      radius = max(radius, 0)
    }

    let innerShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath
    ctx.addPath(innerShadowPath)
    ctx.clip()

    let shadowPath = CGMutablePath()
    let shadowRect = rect.insetBy(dx: -rect.size.width, dy: -rect.size.width)
    shadowPath.addRect(shadowRect)
    shadowPath.addPath(innerShadowPath)
    shadowPath.closeSubpath()

    if let innerShadowColor = innerShadowColor, let oldComponents = innerShadowColor.components {
      var newComponets = Array<CGFloat>(repeating: 0, count: 4) // [0, 0, 0, 0] as [CGFloat]
      let numberOfComponents = innerShadowColor.numberOfComponents

      switch numberOfComponents {
      case 2:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[0]
        newComponets[2] = oldComponents[0]
        newComponets[3] = oldComponents[1] * CGFloat(innerShadowOpacity)
      case 4:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[1]
        newComponets[2] = oldComponents[2]
        newComponets[3] = oldComponents[3] * CGFloat(innerShadowOpacity)
      default:
        break
      }

      if let innerShadowColorWithMultipliedAlpha = CGColor(colorSpace: colorspace, components: newComponets) {
        ctx.setFillColor(innerShadowColorWithMultipliedAlpha)
        ctx.setShadow(offset: innerShadowOffset, blur: innerShadowRadius, color: innerShadowColorWithMultipliedAlpha)
        ctx.addPath(shadowPath)
        ctx.fillPath(using: .evenOdd)
      }
    } 
  }
}
Nike Kov
  • 12,630
  • 8
  • 75
  • 122
Arco
  • 614
  • 5
  • 13
  • What if I am not using it as a seperate class, but like using in my code, the context (ctx) is nil when i get this: `let ctx = UIGraphicsGetCurrentContext` – Mohsin Khubaib Ahmed Dec 01 '18 at 11:44
  • @MohsinKhubaibAhmed You can get current context by method `UIGraphicsGetCurrentContext ` to fetch when some views push their context onto the stack. – Arco Dec 03 '18 at 03:03
  • @Arco I had some trouble when I rotated the device. I added 'override convenience init(layer: Any) { self.init() }'. Now no error displayed! – Yuma Technical Inc. Mar 17 '19 at 20:00
  • Added init(layer: Any) to fix crash. – Nike Kov Apr 23 '19 at 16:40
3

I'm very late to the the party but I'd like to give back to the community.. This is a method I wrote to remove UITextField background Image as I was supplying a Static Library and NO Resources... I used this for a PIN Entry screen of four UITextField instances that could display One character raw or (BOOL)[self isUsingBullets] or (BOOL)[self usingAsterisks] in ViewController. App is for iPhone/iPhone retina/iPad/iPad Retina so I do not have to supply four images...

#import <QuartzCore/QuartzCore.h>

- (void)setTextFieldInnerGradient:(UITextField *)textField
{

    [textField setSecureTextEntry:self.isUsingBullets];
    [textField setBackgroundColor:[UIColor blackColor]];
    [textField setTextColor:[UIColor blackColor]];
    [textField setBorderStyle:UITextBorderStyleNone];
    [textField setClipsToBounds:YES];

    [textField.layer setBorderColor:[[UIColor blackColor] CGColor]];
    [textField.layer setBorderWidth:1.0f];

    // make a gradient off-white background
    CAGradientLayer *gradient = [CAGradientLayer layer];
    CGRect gradRect = CGRectInset([textField bounds], 3, 3);    // Reduce Width and Height and center layer
    gradRect.size.height += 2;  // minimise Bottom shadow, rely on clipping to remove these 2 pts.

    gradient.frame = gradRect;
    struct CGColor *topColor = [UIColor colorWithWhite:0.6f alpha:1.0f].CGColor;
    struct CGColor *bottomColor = [UIColor colorWithWhite:0.9f alpha:1.0f].CGColor;
    // We need to use this fancy __bridge object in order to get the array we want.
    gradient.colors = [NSArray arrayWithObjects:(__bridge id)topColor, (__bridge id)bottomColor, nil];
    [gradient setCornerRadius:4.0f];
    [gradient setShadowOffset:CGSizeMake(0, 0)];
    [gradient setShadowColor:[[UIColor whiteColor] CGColor]];
    [gradient setShadowOpacity:1.0f];
    [gradient setShadowRadius:3.0f];

    // Now we need to Blur the edges of this layer "so it blends"
    // This rasterizes the view down to 4x4 pixel chunks then scales it back up using bilinear filtering...
    // it's EXTREMELY fast and looks ok if you are just wanting to blur a background view under a modal view.
    // To undo it, just set the rasterization scale back to 1.0 or turn off rasterization.
    [gradient setRasterizationScale:0.25];
    [gradient setShouldRasterize:YES];

    [textField.layer insertSublayer:gradient atIndex:0];

    if (self.usingAsterisks) {
        [textField setFont:[UIFont systemFontOfSize:80.0]];
    } else {
        [textField setFont:[UIFont systemFontOfSize:40.0]];
    }
    [textField setTextAlignment:UITextAlignmentCenter];
    [textField setEnabled:NO];
}

I hope this helps someone as this forum has helped me.

Foxnolds
  • 69
  • 1
3

Check the great article of Inner Shadows in Quartz by Chris Emery wich explains how the inner shadows are drawn by PaintCode and gives a clean and neat code snippet:

- (void)drawInnerShadowInContext:(CGContextRef)context
                        withPath:(CGPathRef)path
                     shadowColor:(CGColorRef)shadowColor
                          offset:(CGSize)offset
                      blurRadius:(CGFloat)blurRadius 
{
    CGContextSaveGState(context);

    CGContextAddPath(context, path);
    CGContextClip(context);

    CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0);

    CGContextSetAlpha(context, CGColorGetAlpha(shadowColor));
    CGContextBeginTransparencyLayer(context, NULL);
        CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextSetFillColorWithColor(context, opaqueShadowColor);
        CGContextAddPath(context, path);
        CGContextFillPath(context);
    CGContextEndTransparencyLayer(context);

    CGContextRestoreGState(context);

    CGColorRelease(opaqueShadowColor);
}
voiger
  • 781
  • 9
  • 19
3

Scalable solution using CALayer in Swift

With the described InnerShadowLayer you can also enable inner shadows for specific edges only, excluding others. (e.g. you can enable inner shadows only on the left and top edges of your view)

You can then add a InnerShadowLayer to your view using:

init(...) {

    // ... your initialization code ...

    super.init(frame: .zero)
    layer.addSublayer(shadowLayer)
}

public override func layoutSubviews() {
    super.layoutSubviews()
    shadowLayer.frame = bounds
}

InnerShadowLayer implementation

/// Shadow is a struct defining the different kinds of shadows
public struct Shadow {
    let x: CGFloat
    let y: CGFloat
    let blur: CGFloat
    let opacity: CGFloat
    let color: UIColor
}

/// A layer that applies an inner shadow to the specified edges of either its path or its bounds
public class InnerShadowLayer: CALayer {
    private let shadow: Shadow
    private let edge: UIRectEdge

    public init(shadow: Shadow, edge: UIRectEdge) {
        self.shadow = shadow
        self.edge = edge
        super.init()
        setupShadow()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func layoutSublayers() {
        updateShadow()
    }

    private func setupShadow() {
        shadowColor = shadow.color.cgColor
        shadowOpacity = Float(shadow.opacity)
        shadowRadius = shadow.blur / 2.0
        masksToBounds = true
    }

    private func updateShadow() {
        shadowOffset = {
            let topWidth: CGFloat = 0
            let leftWidth = edge.contains(.left) ? shadow.y / 2 : 0
            let bottomWidth: CGFloat = 0
            let rightWidth = edge.contains(.right) ? -shadow.y / 2 : 0

            let topHeight = edge.contains(.top) ? shadow.y / 2 : 0
            let leftHeight: CGFloat = 0
            let bottomHeight = edge.contains(.bottom) ? -shadow.y / 2 : 0
            let rightHeight: CGFloat = 0

            return CGSize(width: [topWidth, leftWidth, bottomWidth, rightWidth].reduce(0, +),
                          height: [topHeight, leftHeight, bottomHeight, rightHeight].reduce(0, +))
        }()

        let insets = UIEdgeInsets(top: edge.contains(.top) ? -bounds.height : 0,
                                  left: edge.contains(.left) ? -bounds.width : 0,
                                  bottom: edge.contains(.bottom) ? -bounds.height : 0,
                                  right: edge.contains(.right) ? -bounds.width : 0)
        let path = UIBezierPath(rect: bounds.inset(by: insets))
        let cutout = UIBezierPath(rect: bounds).reversing()
        path.append(cutout)
        shadowPath = path.cgPath
    }
}

Pietro Basso
  • 1,403
  • 11
  • 11
1

this code worked for me

class InnerDropShadowView: UIView {
    override func draw(_ rect: CGRect) {
        //Drawing code
        let context = UIGraphicsGetCurrentContext()
        //// Shadow Declarations
        let shadow: UIColor? = UIColor.init(hexString: "a3a3a3", alpha: 1.0) //UIColor.black.withAlphaComponent(0.6) //UIColor.init(hexString: "d7d7da", alpha: 1.0)
        let shadowOffset = CGSize(width: 0, height: 0)
        let shadowBlurRadius: CGFloat = 7.5
        //// Rectangle Drawing
        let rectanglePath = UIBezierPath(rect: bounds)
        UIColor.groupTableViewBackground.setFill()
        rectanglePath.fill()
        ////// Rectangle Inner Shadow
        context?.saveGState()
        UIRectClip(rectanglePath.bounds)
        context?.setShadow(offset: CGSize.zero, blur: 0, color: nil)
        context?.setAlpha((shadow?.cgColor.alpha)!)
        context?.beginTransparencyLayer(auxiliaryInfo: nil)
        do {
            let opaqueShadow: UIColor? = shadow?.withAlphaComponent(1)
            context?.setShadow(offset: shadowOffset, blur: shadowBlurRadius, color: opaqueShadow?.cgColor)
            context!.setBlendMode(.sourceOut)
            context?.beginTransparencyLayer(auxiliaryInfo: nil)
            opaqueShadow?.setFill()
            rectanglePath.fill()
            context!.endTransparencyLayer()
        }
        context!.endTransparencyLayer()
        context?.restoreGState()
    }
}
Antony Raphel
  • 2,036
  • 2
  • 25
  • 45
0

There is some code here which can do this for you. If you change the layer in your view (by overriding + (Class)layerClass), to JTAInnerShadowLayer then you can set the inner shadow on the indent layer up in your init method and it will do the work for you. If you also want to draw the original content make sure you call setDrawOriginalImage:yes on the indent layer. There's a blog post about how this works here.

James Snook
  • 4,063
  • 2
  • 18
  • 30
  • @MiteshDobareeya Just tested both the links and they appear to work fine (including in a private tab). Which link was causing you problems? – James Snook Mar 27 '18 at 09:16
  • Can you please look on this implementation of inner shadow code. It is only working in ViewDidAppear method. And Shows some flickering. https://drive.google.com/open?id=1VtCt7UFYteq4UteT0RoFRjMfFnbibD0E – Mitesh Dobareeya Mar 27 '18 at 10:27
0

Using Gradient Layer:

UIView * mapCover = [UIView new];
mapCover.frame = map.frame;
[view addSubview:mapCover];

CAGradientLayer * vertical = [CAGradientLayer layer];
vertical.frame = mapCover.bounds;
vertical.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[UIColor whiteColor].CGColor, nil];
vertical.locations = @[@0.01,@0.1,@0.9,@0.99];
[mapCover.layer insertSublayer:vertical atIndex:0];

CAGradientLayer * horizontal = [CAGradientLayer layer];
horizontal.frame = mapCover.bounds;
horizontal.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[UIColor whiteColor].CGColor, nil];
horizontal.locations = @[@0.01,@0.1,@0.9,@0.99];
horizontal.startPoint = CGPointMake(0.0, 0.5);
horizontal.endPoint = CGPointMake(1.0, 0.5);
[mapCover.layer insertSublayer:horizontal atIndex:0];
Johnny Rockex
  • 4,136
  • 3
  • 35
  • 55
-1

There's a simple solution - just draw the normal shadow and rotate, like this

@objc func shadowView() -> UIView {
        let shadowView = UIView(frame: .zero)
        shadowView.backgroundColor = .white
        shadowView.layer.shadowColor = UIColor.grey.cgColor
        shadowView.layer.shadowOffset = CGSize(width: 0, height: 2)
        shadowView.layer.shadowOpacity = 1.0
        shadowView.layer.shadowRadius = 4
        shadowView.layer.compositingFilter = "multiplyBlendMode"
        return shadowView
    }

func idtm_addBottomShadow() {
        let shadow = shadowView()
        shadow.transform = transform.rotated(by: 180 * CGFloat(Double.pi))
        shadow.transform = transform.rotated(by: -1 * CGFloat(Double.pi))
        shadow.translatesAutoresizingMaskIntoConstraints = false
        addSubview(shadow)
        NSLayoutConstraint.activate([
            shadow.leadingAnchor.constraint(equalTo: leadingAnchor),
            shadow.trailingAnchor.constraint(equalTo: trailingAnchor),
            shadow.bottomAnchor.constraint(equalTo: bottomAnchor),
            shadow.heightAnchor.constraint(equalToConstant: 1),
            ])
    }
iago849
  • 1,818
  • 1
  • 13
  • 9