Search code examples
iosswiftuikitswift5

UIButton with cornerRadius, shadow and background for state


This seems to be a tricky problem I cannot find the solution for.

I need a UIButton with:

  1. rounded corners,
  2. background color for state,
  3. drop shadow.

I can achieve the the corner radius and background color for state subclassing a UIButton and setting the parameters:

// The button is set to "type: Custom" in the interface builder.

// 1. rounded corners
self.layer.cornerRadius = 10.0

// 2. Color for state
self.backgroundColor = UIColor.orange // for state .normal
self.setBackgroundColor(color: UIColor.red, forState: .highlighted)

// 3. Add the shadow
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 6.0
self.layer.masksToBounds = false


// I'm also using an extension to be able to set the background color
extension UIButton {
    func setBackgroundColor(color: UIColor, forState: UIControl.State) {        
        UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
        if let context = UIGraphicsGetCurrentContext() {
            context.setFillColor(color.cgColor)
            context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
            let colorImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            self.setBackgroundImage(colorImage, for: forState)
        }
    }
}

The problem:
Once the button is rendered, seems fine, has the desired background color, rounded corners and the shadow. But once is pressed the setBackgroundImage(image, for: forState) launches and gets rid of the radius, so the button is displayed as a square with the desired color and shadow.

Is there a way to preserve the radius when the button is pressed (.highlighted)?

I did try the solutions from this post (for example) but none consider the setBackgroundImage(image, for: forState). I cannot find anything that works...


Solution

  • If you just want to change the background color for .normal and .highlighted - that is, you don't need a background image - you can override var isHighlighted to handle the color changes.

    Here is an example UIButton subclass. It is marked @IBDesignable and has normalBackground and highlightBackground colors marked as @IBInspectable so you can set them in Storyboard / Interface Builder:

    @IBDesignable
    class HighlightButton: UIButton {
    
        @IBInspectable
        var normalBackground: UIColor = .clear {
            didSet {
                backgroundColor = self.normalBackground
            }
        }
    
        @IBInspectable
        var highlightBackground: UIColor = .clear
    
        override open var isHighlighted: Bool {
            didSet {
                backgroundColor = isHighlighted ? highlightBackground : normalBackground
            }
        }
    
        func setBackgroundColor(_ c: UIColor, forState: UIControl.State) -> Void {
            if forState == UIControl.State.normal {
                normalBackground = c
            } else if forState == UIControl.State.highlighted {
                highlightBackground = c
            } else {
                // implement other states as desired
            }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
    
        func commonInit() -> Void {
    
            // 1. rounded corners
            self.layer.cornerRadius = 10.0
    
            // 2. Default Colors for state
            self.setBackgroundColor(.orange, forState: .normal)
            self.setBackgroundColor(.red, forState: .highlighted)
    
            // 3. Add the shadow
            self.layer.shadowColor = UIColor.black.cgColor
            self.layer.shadowOffset = CGSize(width: 0, height: 3)
            self.layer.shadowOpacity = 0.3
            self.layer.shadowRadius = 6.0
            self.layer.masksToBounds = false
    
        }
    
    }