138

I have designed UI elements in sketch, and one of them has a shadow with blur 1 and spread 0. I looked at the doc for the views layer property and layer doesn't have anything named spread or blur, or anything equivalent (the only control was merely shadowOpacity). How can control things like blur and spread?

Here are my settings in Sketch:

Sketch shadow settings

And here is what I want my shadow to look like:

Shadow wanted

And here is what it looks like at the moment:

Current shadow

Note, you have to click on the picture to actually see the shadow.

My code is as follows:

func setupLayer(){
    view.layer.cornerRadius = 2
    view.layer.shadowColor = Colors.Shadow.CGColor
    view.layer.shadowOffset = CGSize(width: 0, height: 1)
    view.layer.shadowOpacity = 0.9
    view.layer.shadowRadius = 5
}
Null
  • 1,950
  • 9
  • 30
  • 33
Quantaliinuxite
  • 3,133
  • 4
  • 18
  • 32
  • The tags ios(the platform), design (the use of the software Sketch) and Core-Graphics (it's possible to use a UIBezierPath to draw the shadow, whih might be relevant) are all relevant, I don't see why they should be removed. – Quantaliinuxite Dec 14 '15 at 14:52
  • you want shadow for that white view only right? – O-mkar Dec 14 '15 at 14:58
  • 8
    Looks like Sketch and CoreAnimation framework have different metrics, because it always looks different on iOS and in Sketch with same params. – Timur Bernikovich Jan 27 '17 at 10:38
  • Ah. The joys of working with designers who use tools that bare little or no resemblance to the way iOS works. If you move to something like PaintCode instead of Sketch it will not only work like iOS works but it will give you the code you need too. :-) – Fogmeister Aug 02 '17 at 18:17
  • What if you have set both the radius and the blur in sketch? – Vladimir Oct 31 '18 at 10:23

8 Answers8

301

Here's how to apply all 6 Sketch shadow properties to a UIView's layer with near perfect accuracy:

extension CALayer {
  func applySketchShadow(
    color: UIColor = .black,
    alpha: Float = 0.5,
    x: CGFloat = 0,
    y: CGFloat = 2,
    blur: CGFloat = 4,
    spread: CGFloat = 0)
  {
    masksToBounds = false
    shadowColor = color.cgColor
    shadowOpacity = alpha
    shadowOffset = CGSize(width: x, height: y)
    shadowRadius = blur / 2.0
    if spread == 0 {
      shadowPath = nil
    } else {
      let dx = -spread
      let rect = bounds.insetBy(dx: dx, dy: dx)
      shadowPath = UIBezierPath(rect: rect).cgPath
    }
  }
}

Say we want to represent the following:

enter image description here

You can easily do this via:

myView.layer.applySketchShadow(
  color: .black, 
  alpha: 0.5, 
  x: 0, 
  y: 0, 
  blur: 4, 
  spread: 0)

or more succinctly:

myView.layer.applySketchShadow(y: 0)

Example:

enter image description here

Left: iPhone 8 UIView screenshot; right: Sketch rectangle.

Note:

  • When using a non-zero spread, it hardcodes a path based on the bounds of the CALayer. If the layer's bounds ever change, you'd want to call the applySketchShadow() method again.
Elijah
  • 8,381
  • 2
  • 55
  • 49
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • Hm. In Sketch, if you apply spread, it 'moves out', where the blur/gradient of the shadow begins. Which means - it stays at the constant shadow-colour, until the blur begins. But if we move the shadowLayer around, the shadow would only begin from that layer, right? (Say spread is 10, the shadow would only begin after an offset of 10, where as in Sketch, the blur/gradient would begin after an offset of 10). So they don't really match up, right? – Stephan Mar 15 '18 at 11:36
  • 2
    @Senseful I'm interested to know how you achieved this formula?! – Hashem Aboonajmi Apr 14 '18 at 17:06
  • I'm not sure this should be the accepted answer. I testing this out, and there seem to be some cases where the shadows apply to the subviews instead of the view I actually add it to – daredevil1234 Apr 27 '18 at 19:11
  • 4
    You forgot to set `maskToBounds = false`. – ScottyBlades Aug 23 '18 at 04:34
  • 1
    How do you know that blur = 4 translates to shadowRadius 4 / 2.0 ? I would like to understand how they are related. Thank you. – matchifang Nov 26 '18 at 16:44
  • 1
    As mentioned above by ScottyBlades, this won't work without `maskToBounds = false`. An example where it will fail is when applying this shadow to an `UIButton`. – José Jan 08 '19 at 20:33
  • @daredevil1234 `CALayer` shadows will _always_ be applied to rasterized layer contents, which includes all sublayers. If you want to exclude sublayers, you need to move the layer you want to apply a shadow to up in the hierarchy so it has no children and only has sibling layers. So it's more about how you structure your layers than it is an incorrect solution. – CIFilter Apr 09 '19 at 19:01
  • 9
    Should this be `shadowRadius = blur / UIScreen.main.scale` as opposed to hard-coding `blur / 2.0`? Or, is there another reason to divide by 2? @matchifang Did you figure this out? – JWK Jun 06 '19 at 06:50
  • Thanks! Maybe for better clarification x/y/blur should be named x/y/blurInPoints or x/y/blurInPixels, whatever is correct :) – Jens Schwarzer Aug 08 '19 at 09:00
  • Great simple and user-friendly solution! Thanx for all who participated in! – eli7ah Oct 06 '20 at 15:25
  • What's dx? I'm not seeing it defined anywhere – Chris Gomez Jan 22 '21 at 20:08
59

You can try this .... you can play with the values. The shadowRadius dictates the amount of blur. shadowOffset dictates where the shadow goes.

Swift 2.0

let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height

demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4)  //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds =  false
demoView.layer.shadowPath = shadowPath.CGPath

Swift 3.0

let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height 
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height)) 
//Change 2.1 to amount of spread you need and for height replace the code for height

demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.black.cgColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4)  //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds =  false
demoView.layer.shadowPath = shadowPath.cgPath

Example with spread

Example with spread

To create a basic shadow

    demoView.layer.cornerRadius = 2
    demoView.layer.shadowColor = UIColor.blackColor().CGColor
    demoView.layer.shadowOffset = CGSizeMake(0.5, 4.0); //Here your control your spread
    demoView.layer.shadowOpacity = 0.5 
    demoView.layer.shadowRadius = 5.0 //Here your control your blur

Basic Shadow example in Swift 2.0

OUTPUT

O-mkar
  • 5,430
  • 8
  • 37
  • 61
15

Sketch Shadow Using IBDesignable and IBInspectable in Swift 4

SKETCH AND XCODE SIDE BY SIDE

Shadow Exampl

CODE

@IBDesignable class ShadowView: UIView {

    @IBInspectable var shadowColor: UIColor? {
        get {
            if let color = layer.shadowColor {
                return UIColor(cgColor: color)
            }
            return nil
        }
        set {
            if let color = newValue {
                layer.shadowColor = color.cgColor
            } else {
                layer.shadowColor = nil
            }
        }
    }

    @IBInspectable var shadowOpacity: Float {
        get {
            return layer.shadowOpacity
        }
        set {
            layer.shadowOpacity = newValue
        }
    }

    @IBInspectable var shadowOffset: CGPoint {
        get {
            return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
        }
        set {
            layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
        }

     }

    @IBInspectable var shadowBlur: CGFloat {
        get {
            return layer.shadowRadius
        }
        set {
            layer.shadowRadius = newValue / 2.0
        }
    }

    @IBInspectable var shadowSpread: CGFloat = 0 {
        didSet {
            if shadowSpread == 0 {
                layer.shadowPath = nil
            } else {
                let dx = -shadowSpread
                let rect = bounds.insetBy(dx: dx, dy: dx)
                layer.shadowPath = UIBezierPath(rect: rect).cgPath
            }
        }
    }
}

OUTPUT

DEMO OUTPUT

HOW TO USE IT

DEMO

O-mkar
  • 5,430
  • 8
  • 37
  • 61
8

This code worked very well for me:

yourView.layer.shadowOpacity = 0.2 // opacity, 20%
yourView.layer.shadowColor = UIColor.black.cgColor
yourView.layer.shadowRadius = 2 // HALF of blur
yourView.layer.shadowOffset = CGSize(width: 0, height: 2) // Spread x, y
yourView.layer.masksToBounds = false
Cesare
  • 9,139
  • 16
  • 78
  • 130
3

For those who are attempting to apply a shadow to a predefined path (Like for a circular view, for instance), here's what I ended up with:

extension CALayer {
    func applyShadow(color: UIColor = .black,
                     alpha: Float = 0.5,
                     x: CGFloat = 0,
                     y: CGFloat = 2,
                     blur: CGFloat = 4,
                     spread: CGFloat = 0,
                     path: UIBezierPath? = nil) {
        shadowColor = color.cgColor
        shadowOpacity = alpha
        shadowRadius = blur / 2
        if let path = path {
            if spread == 0 {
                shadowOffset = CGSize(width: x, height: y)
            } else {
                let scaleX = (path.bounds.width + (spread * 2)) / path.bounds.width
                let scaleY = (path.bounds.height + (spread * 2)) / path.bounds.height

                path.apply(CGAffineTransform(translationX: x + -spread, y: y + -spread).scaledBy(x: scaleX, y: scaleY))
                shadowPath = path.cgPath
            }
        } else {
            shadowOffset = CGSize(width: x, height: y)
            if spread == 0 {
                shadowPath = nil
            } else {
                let dx = -spread
                let rect = bounds.insetBy(dx: dx, dy: dx)
                shadowPath = UIBezierPath(rect: rect).cgPath
            }
        }
        shouldRasterize = true
        rasterizationScale = UIScreen.main.scale
    }
}

I'll post some examples later, but this has worked spot on for circular views for me.

Zig
  • 2,603
  • 1
  • 14
  • 20
  • This "almost" works but the spread is wrong when you use a spread of `1` with `x` and `y` `0` `shapeLayer.applyShadow(color: .black, alpha: 1, x: 0, y: 0, blur: 0, spread: 1, path: path)`. In this case the shadow has an offset while it should not. Here the triangle should have the shadow spread 1px in all directions https://ibb.co/YjwRb9B – Jan Jul 04 '19 at 08:10
1

My solution based on this post replies: (Swift 3)

let shadowPath = UIBezierPath(rect: CGRect(x: -1,
                                           y: -2,
                                           width: target.frame.width + 2,
                                           height: target.frame.height + 2))

target.layer.shadowColor = UIColor(hexString: shadowColor).cgColor
target.layer.shadowOffset = CGSize(width: CGFloat(shadowOffsetX), height: CGFloat(shadowOffsetY))
target.layer.masksToBounds = false
target.layer.shadowOpacity = Float(shadowOpacity)
target.layer.shadowPath = shadowPath.cgPath
Josep Escobar
  • 445
  • 4
  • 11
1

Change a little bit from @Senseful 's answer and works fine to my project.

  • Support shadow corner radius (for some round corner view)
  • spread == 0 it still apply shadow effect(looks like border)
struct SketchShadow {
    var color: UIColor = .black
    let alpha: Float = 0.1
    let x: CGFloat
    let y: CGFloat
    let blur: CGFloat
    let spread: CGFloat
    let cornorRadius: CGFloat
    
    func applyToLayer(_ layer: CALayer) {
        layer.masksToBounds = false
        layer.shadowColor = color.cgColor
        layer.shadowOpacity = alpha
        layer.shadowOffset = CGSize(width: x, height: y)
        layer.shadowRadius = blur / 2.0
        if spread == 0 {
            layer.shadowPath = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornorRadius, height: cornorRadius)).cgPath
        } else {
            let dx = -(spread)
            let rect = layer.bounds.insetBy(dx: dx, dy: dx)
            layer.shadowPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornorRadius, height: cornorRadius)).cgPath
        }
    }
}
Roger Lee
  • 200
  • 1
  • 7
0

I really like the answer posted here and suggestions in the comments. This is how I modified that solution:

extension UIView {
  func applyShadow(color: UIColor, alpha: Float, x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat) {
    layer.masksToBounds = false
    layer.shadowColor = color.cgColor
    layer.shadowOpacity = alpha
    layer.shadowOffset = CGSize(width: x, height: y)
    layer.shadowRadius = blur / UIScreen.main.scale
    if spread == 0 {
      layer.shadowPath = nil
    } else {
      let dx = -spread
      let rect = bounds.insetBy(dx: dx, dy: dx)
      layer.shadowPath = UIBezierPath(rect: rect).cgPath
    }
  }
}

USAGE:

myButton.applyShadow(color: .black, alpha: 0.2, x: 0, y: 1, blur: 2, spread: 0)
budiDino
  • 13,044
  • 8
  • 95
  • 91