6

Background

I have a UIView with the following properties:

  • alpha = 1
  • backgroundColor = white, with 0.35 opacity
  • rounded corners
  • drop shadow

Code

This is how I create the drop shadow: (UIView extension)

self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.darkGrayColor().CGColor
self.layer.shadowOffset = CGSizeMake(0, 5)
self.layer.shadowOpacity = 0.35
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).CGPath

Results

This results in the following:

current result

...while I do not want to see the shadow beneath the view like this:

wished result

Question

How can I draw the shadow outside the view only so it is not visible below it?

Thanks in advance!

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174

3 Answers3

6

This is perhaps slightly more complex than it would need to be, but here's one solution.

Extend UIView with the following method:

extension UIView {
    // Note: the method needs the view from which the context is taken as an argument.
    func dropShadow(superview: UIView) {
        // Get context from superview
        UIGraphicsBeginImageContext(self.bounds.size)
        superview.drawViewHierarchyInRect(CGRect(x: -self.frame.minX, y: -self.frame.minY, width: superview.bounds.width, height: superview.bounds.height), afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        // Add a UIImageView with the image from the context as a subview
        let imageView = UIImageView(frame: self.bounds)
        imageView.image = image
        imageView.layer.cornerRadius = self.layer.cornerRadius
        imageView.clipsToBounds = true
        self.addSubview(imageView)

        // Bring the background color to the front, alternatively set it as UIColor(white: 1, alpha: 0.2)
        let brighter = UIView(frame: self.bounds)
        brighter.backgroundColor = self.backgroundColor ?? UIColor(white: 1, alpha: 0.2)
        brighter.layer.cornerRadius = self.layer.cornerRadius
        brighter.clipsToBounds = true
        self.addSubview(brighter)

        // Set the shadow
        self.layer.masksToBounds = false
        self.layer.shadowColor = UIColor.darkGrayColor().CGColor
        self.layer.shadowOffset = CGSizeMake(0, 5)
        self.layer.shadowOpacity = 0.35
        self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).CGPath
    }
}

Usage, considering the background view is named view:

let shadowView = UIView(frame: CGRect(x: 100, y: 100, width: 300, height: 200))
shadowView.layer.cornerRadius = 15.0
shadowView.dropShadow(view)

view.addSubview(shadowView)

Which results in a view like this:

UIView

Note: the dropShadow method can not be called from viewDidLoad as this causes issues with the graphics context. So, use this method in viewWillAppear earliest for the above result.


Here's the code for the background view, just in case somebody wants to test in playgrounds:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 400))
view.backgroundColor = UIColor.clearColor()

let color1 = UIColor(hue: 0.39, saturation: 0.7, brightness: 1.0, alpha: 1.0).CGColor
let color2 = UIColor(hue: 0.51, saturation: 0.9, brightness: 0.6, alpha: 1.0).CGColor

let gradient = CAGradientLayer()
gradient.frame = view.frame
gradient.colors = [color1, color2]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
view.layer.insertSublayer(gradient, atIndex: 0)
xoudini
  • 7,001
  • 5
  • 23
  • 37
  • Looks nice, but for me [this is the result](https://www.dropbox.com/s/9flp6g1s98hjt9o/Bildschirmfoto%202016-07-27%20um%2021.57.17.png?dl=0). – LinusGeffarth Jul 27 '16 at 20:15
  • Sorry, should've probably clarified: the `superview` parameter needs the view that the "shadowView" gets its context from. So if you want it to imitate the background named `backgroundView`, you need to pass that to the method as such: `shadowView.dropShadow(backgroundView)` – xoudini Jul 27 '16 at 20:20
  • What exactly do you mean by "imitate"? So, I have a white view with rounded corners and opacity which I want to have the drop shadow. Its superview is simply `view` so I passed `view` as parameter. Somehow that is not correct though... – LinusGeffarth Jul 27 '16 at 20:32
  • Just added a sample background I used for that exact result. By "imitate" I mean that the method needs the background with an image or gradient or whatever to create an image from its context. – xoudini Jul 27 '16 at 20:35
  • Ok maybe I'm just really dumb. Now the whole screen turns green. When I print the view it tells me though, that its frame is as small as it's supposed to be. Do you mind sharing your project? – LinusGeffarth Jul 27 '16 at 20:44
  • 1
    Try it out in playgrounds - copy the background first, then the extension and then the usage example. That should clarify slightly, otherwise we should probably move this to chat :) – xoudini Jul 27 '16 at 20:48
6

I usually solve this kind of situation using composition of views. Instead of setting the shadow in the target view, I create a ShadowView and I put it behind the target view, with the same frame.

Doing so, we can mask the shadow view in order to only draw the shadow outside its frame.

The code for the shadow's mask is (full code above):

      let maskLayer = CAShapeLayer()
      let path = CGMutablePath()
      path.addPath(UIBezierPath(roundedRect: bounds.inset(by: UIEdgeInsets.zero), cornerRadius: CGFloat(cornerRadius)).cgPath)
      path.addPath(UIBezierPath(roundedRect: bounds.inset(by: UIEdgeInsets(top: -offset.height - radius*2, left: -offset.width - radius*2, bottom: -offset.height - radius*2, right: -offset.width - radius*2)), cornerRadius: CGFloat(cornerRadius)).cgPath)
      maskLayer.backgroundColor = UIColor.black.cgColor
      maskLayer.path = path;
      maskLayer.fillRule = .evenOdd
      self.layer.mask = maskLayer

The result is the following:

Masked shadow

Full playground available here: https://gist.github.com/llinardos/d1ede3b491efc8edac940c5ea8631c6f

This kind of effect looks really nice with blurred backgrounds.

kanobius
  • 756
  • 9
  • 12
0

This was adapted a bit from a post found here. I did not know the specifics of your layer so I created a circle centered in the middle of the screen instead. You could easily replace the circle with the specifications of your layer and I believe that will correct your issues. Feel free to comment for any further instruction! import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    createOverlay(view.bounds)
}
func createOverlay(frame : CGRect)
{
    let overlayView = UIView(frame: frame)
    overlayView.alpha = 0.6
    overlayView.backgroundColor = UIColor.blackColor()
    self.view.addSubview(overlayView)

    let maskLayer = CAShapeLayer()

    let path = CGPathCreateMutable()

    let radius : CGFloat = 50.0
    let xOffset : CGFloat = view.bounds.width / 2
    let yOffset : CGFloat = view.bounds.height / 2

    CGPathAddArc(path, nil, overlayView.frame.width - xOffset, yOffset, radius, 0.0, 2 * 3.14, false)
    CGPathAddRect(path, nil, CGRectMake(0, 0, overlayView.frame.width, overlayView.frame.height))
    maskLayer.backgroundColor = UIColor.blackColor().CGColor
    maskLayer.path = path;
    maskLayer.fillRule = kCAFillRuleEvenOdd

    overlayView.layer.mask = maskLayer
    overlayView.clipsToBounds = true
}
}

Photo Simulator Screenshot