Search code examples
swiftuikit

How to make two shadows on one UIView?


Good afternoon everyone. Tell me how to make two effects on one View.

I tried to do it through CALayer. But this method doesn't work!

// Create UIView.
let testView = UIView()     
testView.backgroundColor = UIColor.gray
testView.layer.borderWidth = 1
testView.translatesAutoresizingMaskIntoConstraints = false

// Create layer with shadow.
let shadowUp = createShadowDown(buttonMenu)

// Add layer with shadow to UIView.
testView.layer.insertSublayer(layerOne, at: 0)

// Method create shadow.
func createShadowUp(_ size: UIView) -> CALayer {
    let layer = CALayer()
    layer.frame = size.bounds
    layer.shadowColor = UIColor.red.cgColor
    layer.shadowOpacity = 1
    layer.shadowRadius = 10
    layer.shadowOffset = CGSize(width: 0, height: 20)
    return layer
}

The second method works, but you can't make two shadows! Here it most likely overwrites the values.

// Create UIView.
let testView = UIView()     
testView.backgroundColor = UIColor.gray
testView.layer.borderWidth = 1
testView.translatesAutoresizingMaskIntoConstraints = false

// Create shadow.
createShadowTest(testView))

// Method create shadow.
func createShadowTest(_ view: UIView) {
    view.layer.shadowColor = UIColor.red.cgColor
    view.layer.shadowOpacity = 1
    view.layer.shadowRadius = 10
    view.layer.shadowOffset = CGSize(width: 0, height: 20)
}

Is it really not possible to add two shadows for one View?

I looked at the solutions on this site, but they don't work! For example here - Adding multiple shadows hides the background colour and text UIButton

// Create UIView.
let testView = UIView()     
testView.backgroundColor = UIColor.gray
testView.layer.borderWidth = 1
testView.translatesAutoresizingMaskIntoConstraints = false

// Create shadow.
addDropShadow(testView))

// Method create shadow.
func addDropShadow() {
    let topLayer = createShadowLayer(color: .red, offset: CGSize(width: 0, height: -20))
    let bottomLayer = createShadowLayer(color: .green, offset: CGSize(width: 0, height: 20))
    self.testView.layer.insertSublayer(topLayer, at: 0)
    self.testView.layer.insertSublayer(bottomLayer, at: 1)
}

func createShadowLayer(color: UIColor, offset: CGSize) -> CALayer {
    let shadowLayer = CALayer()
    shadowLayer.masksToBounds = false
    shadowLayer.shadowColor = color.cgColor
    shadowLayer.shadowOpacity = 1
    shadowLayer.shadowOffset = offset
    shadowLayer.shadowRadius = 10
    shadowLayer.shouldRasterize = true
    shadowLayer.shadowPath = UIBezierPath(roundedRect: testView.bounds, cornerRadius: 10).cgPath

    return shadowLayer
}

Solution

  • There are two problems here:

    1. The UIView was instantiated as UIView(). That means that its frame.size is .zero.

    2. A more subtle issue is that you set the shadowPath using the then-current bounds. But if the view frame is subsequently changed (whether manually or via layout constraints), the shadowPath will not automatically update its path to reflect the new size.

    I would suggest moving this shadowPath adjustment logic into a UIView subclass, so that as the view resizes, the respective shadowPath values can be updated (in its layoutSubviews method):

    class ViewWithShadows: UIView {
        private var shadowLayers: [CALayer] = []
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            let path = shadowPath.cgPath
            
            for shadowLayer in shadowLayers {
                shadowLayer.shadowPath = path
            }
        }
    
        func addShadowLayer(color: UIColor, offset: CGSize = .zero, radius: CGFloat = 10) {
            let sublayer = shadowLayer(color: color, offset: offset, radius: radius)
            layer.addSublayer(sublayer)
            shadowLayers.append(sublayer)
        }
        
        func insertShadowLayer(color: UIColor, offset: CGSize = .zero, radius: CGFloat = 10, at index: UInt32) {
            let sublayer = shadowLayer(color: color, offset: offset, radius: radius)
            layer.insertSublayer(sublayer, at: index)
            shadowLayers.append(sublayer)
        }
    }
    
    private extension ViewWithShadows {
        var shadowPath: UIBezierPath {
            UIBezierPath(roundedRect: bounds, cornerRadius: 10)
        }   
        
        // Method create shadow.
        func shadowLayer(color: UIColor, offset: CGSize, radius: CGFloat) -> CALayer {
            let shadowLayer = CALayer()
            shadowLayer.masksToBounds = false
            shadowLayer.shadowColor = color.cgColor
            shadowLayer.shadowOpacity = 1
            shadowLayer.shadowOffset = offset
            shadowLayer.shadowRadius = radius
            shadowLayer.shadowPath = shadowPath.cgPath
            
            return shadowLayer
        }
    }
    
    class ViewController: UIViewController {    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            addTwoShadowView()
        }
        
        func addTwoShadowView() {
            // Create UIView.
            let subview = ViewWithShadows()
            subview.backgroundColor = .gray
            subview.layer.borderWidth = 1
            subview.translatesAutoresizingMaskIntoConstraints = false
            
            view.addSubview(subview)
            
            NSLayoutConstraint.activate([
                subview.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                subview.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                subview.widthAnchor.constraint(equalToConstant: 100),
                subview.heightAnchor.constraint(equalToConstant: 100)
            ])
            
            subview.insertShadowLayer(color: .red, offset: CGSize(width: 0, height: -20), at: 0)
            subview.insertShadowLayer(color: .green, offset: CGSize(width: 0, height: 20), at: 1)
        }
    }
    

    Anyway, that yields:

    enter image description here

    There are lots of different ways to solve this, and this is just one. But hopefully it illustrates the idea. Just make sure that your shadow path updates as the view’s bounds changes.


    Personally, I would be inclined to not attempt to add different layers, and just have two views, each with a shadow applied to its main layer, and eliminate the shadowPath. That eliminates the logic associated with “adjust the shadowPath when the bounds change”, it will offer more graceful animation should the view’s bounds change (should you need to do that), etc.

    class ViewController: UIViewController {   
        override func viewDidLoad() {
            super.viewDidLoad()
            
            addTwoShadowViews()
        }
    }
    
    private extension ViewController {
        func shadowView(color: UIColor, offset: CGSize, radius: CGFloat = 10) -> UIView {
            let view = UIView()
            view.translatesAutoresizingMaskIntoConstraints = false
            view.backgroundColor = .gray
            view.layer.masksToBounds = false
            view.layer.shadowColor = color.cgColor
            view.layer.shadowOpacity = 1
            view.layer.shadowOffset = offset
            view.layer.shadowRadius = radius
            return view
        }
        
        func addTwoShadowViews() {
            // Create UIView.
            let redShadowView = shadowView(color: .red, offset: CGSize(width: 0, height: -20))
            let greenShadowView = shadowView(color: .green, offset: CGSize(width: 0, height: 20))
                    
            view.addSubview(redShadowView)
            view.addSubview(greenShadowView)
                    
            NSLayoutConstraint.activate([
                redShadowView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                redShadowView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                redShadowView.widthAnchor.constraint(equalToConstant: 100),
                redShadowView.heightAnchor.constraint(equalToConstant: 100),
    
                greenShadowView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                greenShadowView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                greenShadowView.widthAnchor.constraint(equalToConstant: 100),
                greenShadowView.heightAnchor.constraint(equalToConstant: 100)
            ])
        }
    }
    

    That yields:

    enter image description here