2

I'm trying to create a UIButton with rounded corners, a background image and a shadow. Before adding the shadow, everything works fine.

enter image description here

But after adding shadow values, the shadow doesn't show up. Obviously due to clipsToBounds property value being set to true. If I remove that, it looks like this.

enter image description here

Since I need the corner radius as well, I cannot have the clipsToBounds be false.

This is my code.

class CustomButton: UIButton {
    
    var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set {
            layer.cornerRadius = newValue
            clipsToBounds = true
        }
    }
    
    var shadowRadius: CGFloat {
        get {
            return layer.shadowRadius
        }
        set {
            layer.shadowRadius = newValue
        }
    }
    
    var shadowOpacity: Float {
        get {
            return layer.shadowOpacity
        }
        set {
            layer.shadowOpacity = newValue
        }
    }
    
    var shadowOffset: CGSize {
        get {
            return layer.shadowOffset
        }
        set {
            layer.shadowOffset = newValue
        }
    }
    
    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
            }
        }
    }
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
    }
    
}


private lazy var button: CustomButton = {
    let button = CustomButton()
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setBackgroundImage(UIImage(named: "Rectangle"), for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.setTitle("Sign Up", for: .normal)
    button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
    button.cornerRadius = 20
    button.shadowColor = .systemGreen
    button.shadowRadius = 10
    button.shadowOpacity = 1
    button.shadowOffset = CGSize(width: 0, height: 0)
    return button
}()

Is there a workaround to have both the shadow and the corner radius?

Demo project

Isuru
  • 30,617
  • 60
  • 187
  • 303

3 Answers3

1

You need to use two separate views for shadow and image. I can't find any solution to set image, shadow, and corner radius using the same button layer.

Make button's corner radius(clipsToBounds=true) rounded and set the image on it.

Take a shadow view under the button with proper shadow radius and offset.

Shashank Mishra
  • 1,051
  • 7
  • 18
1

You can do it via adding shadow and background image with different layer.

First, if you don't need the properties, remove all and modify your CustomButton implementation just like below (modify as your need):

class CustomButton: UIButton {
    
    private let cornerRadius: CGFloat = 20
    private var imageLayer: CALayer!
    private var shadowLayer: CALayer!
    
    override func draw(_ rect: CGRect) {
        addShadowsLayers(rect)
    }
    
    private func addShadowsLayers(_ rect: CGRect) {
        // Add Image
        if self.imageLayer == nil {
            let imageLayer = CALayer()
            imageLayer.frame = rect
            imageLayer.contents = UIImage(named: "Rectangle")?.cgImage
            imageLayer.cornerRadius = cornerRadius
            imageLayer.masksToBounds = true
            layer.insertSublayer(imageLayer, at: 0)
            self.imageLayer = imageLayer
        }
        
        // Set the shadow
        if self.shadowLayer == nil {
            let shadowLayer = CALayer()
            shadowLayer.masksToBounds = false
            shadowLayer.shadowColor = UIColor.systemGreen.cgColor
            shadowLayer.shadowOffset = .zero
            shadowLayer.shadowOpacity = 1
            shadowLayer.shadowRadius = 10
            shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
            layer.insertSublayer(shadowLayer, at: 0)
            self.shadowLayer = shadowLayer
        }
    }
}

And initialize your button like below:

private lazy var button: CustomButton = {
    let button = CustomButton()
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setTitleColor(.white, for: .normal)
    button.setTitle("Sign Up", for: .normal)
    button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
    return button
}()

enter image description here

Omer Faruk Ozturk
  • 1,722
  • 13
  • 25
  • Thank you. This is great. But the thing is if I remove the properties, I end up hardcoding the values inside the button class itself. I'm hoping to make it more of a generic solution. So I can set the shadow/background image values when initialising the button. – Isuru Jul 06 '20 at 05:18
  • Update: I modified your code like [this](https://pastebin.com/VWRfuPGH) by exposing the properties. Still works great. However I noticed a small problem with the shadow. If I reduce the shadow radius, The shadow doesn't have the same corner radius. [See](https://i.imgur.com/6EMl34D.png). Any idea why that may be? This issue exists in the original code as well. I tried adding the `cornerRadius` value to the `shadowLayer` as well, but that didn't change anything. – Isuru Jul 06 '20 at 06:04
  • Did you give same radius for both? I made a small update for that. (btw i can not access to first link). you can edit/extend any part of the code as your need, i just keep the required part for what i did. – Omer Faruk Ozturk Jul 06 '20 at 06:26
  • Please check now. [link](http://pastie.org/p/0ugkeKMF75NW652vYfRR3z). The `cornerRadius` and `shadowRadius` are different, right? Since shadow radius handles the blur radius of the shadow, I set it to 2. That's when I noticed the squared edges of the shadow. The corner radius is still set to 20. No issue with that. – Isuru Jul 06 '20 at 06:32
  • 1
    Yes you are right, I modified `shadowLayer.shadowPath` line. – Omer Faruk Ozturk Jul 06 '20 at 06:43
  • @omerfarukozturk how to add press animation in your CustomButton class? please help. Thank you. – frank61003 Nov 06 '20 at 03:11
  • Is it possible to use the root layer (by calling `setBackgroundImage`) so we only have 1 sublayer (the shadow's)? Since we can use it to set different images for different states. – CyberMew Nov 10 '21 at 03:46
  • I just tried this out, and it seems like 2 things to note: the sublayer position matters (index 0 is rendered on screen first hence shadow should there), and for some reason `imageLayer.contents` does not work for me for some reason and I realised I was using an `UIImage` instead of `cgImage`. Also it seems like this sublayers solution does not work with the `setBackgroundImage` which is unfortunate.. I think the workaround is to add a view manually behind the button itself. – CyberMew Nov 10 '21 at 04:17
  • I found the solution to it in another question (for reference https://stackoverflow.com/a/37687638/321629) – CyberMew Nov 10 '21 at 07:24
0

You can add view.layer.masksToBounds = false

This will disable clipping for sublayer

https://developer.apple.com/documentation/quartzcore/calayer/1410896-maskstobounds