Search code examples
iosswiftxcodeuibuttonantialiasing

Custom UIButton's edges look pixelated after interacting with it


I've created a custom UIButton subclass with rounded corners, gradient background and shadows. Buttons look ok and antialiased immediately after starting the application, but if I press one it's edges become pixelated.

I've tried a lot of stuff like setting .allowsEdgeAntialiasing = true on button's layers or removing scale transform animation from "Highlighted" setter etc. and nothing helps at all :(

Here is my button class:

@IBDesignable class CircleTintedButton: UIButton {
    @IBInspectable var cornerRadius : CGFloat = 1.0
    @IBInspectable var shadowOffsetWidth: CGFloat = 0.0
    @IBInspectable var shadowOffsetHeight: CGFloat = 2.0
    @IBInspectable var shadowColor : UIColor = UIColor.gray
    @IBInspectable var shadowOpacity: CGFloat = 0.3

    @IBInspectable var startColor: UIColor = .blue {
        didSet {
            setNeedsLayout()
        }
    }

    @IBInspectable var endColor: UIColor = .green {
        didSet {
            setNeedsLayout()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.cornerRadius = cornerRadius
        layer.shadowColor = shadowColor.cgColor
        layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
        let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
        layer.shadowPath = shadowPath.cgPath
        layer.shadowOpacity = Float(shadowOpacity)

        let gradientLayer = CAGradientLayer()
        gradientLayer.colors = [startColor.cgColor, endColor.cgColor]        
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.6)

        gradientLayer.locations = [0, 1]
        gradientLayer.frame = bounds
        gradientLayer.cornerRadius = cornerRadius
        gradientLayer.masksToBounds = true

        layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)        
    }

    override var isHighlighted: Bool {
        get {
            return super.isHighlighted
        }
        set {
            let xScale : CGFloat = newValue ? 1.025 : 1.0
            let yScale : CGFloat = newValue ? 1.1 : 1.0
            UIView.animate(withDuration: 0.1) {
                let transformation = CGAffineTransform(scaleX: xScale, y: yScale)
                self.transform = transformation
            }

            super.isHighlighted = newValue
        }
    }
}

Some screenshots from my test device (iPhone 7 @ 12.1.2):

After app launch: https://vinishko.party/files/ok.jpg

After I press this button: https://vinishko.party/files/aliased.jpg

Spent the whole day already trying to fix this problem, please help me :D Thanks.


Solution

  • You are adding another gradient layer every time layoutSubviews() is called. You can confirm this by adding an @IBAction for your button:

    @IBAction func didTap(_ sender: Any) {
    
        if let b  = sender as? CircleTintedButton {
            let n = b.layer.sublayers?.count
            print("nunLayers: \(String(describing: n))")
        }
    
    }
    

    You'll see that the sublayers count increases with each tap.

    Add a gradient layer as a var / property of your custom button, and then only add it once:

    @IBDesignable class CircleTintedButton: UIButton {
        @IBInspectable var cornerRadius : CGFloat = 1.0
        @IBInspectable var shadowOffsetWidth: CGFloat = 0.0
        @IBInspectable var shadowOffsetHeight: CGFloat = 2.0
        @IBInspectable var shadowColor : UIColor = UIColor.gray
        @IBInspectable var shadowOpacity: CGFloat = 0.3
    
        // add this var / property
        private var gradLayer: CAGradientLayer?
    
        @IBInspectable var startColor: UIColor = .blue {
            didSet {
                setNeedsLayout()
            }
        }
    
        @IBInspectable var endColor: UIColor = .green {
            didSet {
                setNeedsLayout()
            }
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            layer.cornerRadius = cornerRadius
            layer.shadowColor = shadowColor.cgColor
            layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
            let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
            layer.shadowPath = shadowPath.cgPath
            layer.shadowOpacity = Float(shadowOpacity)
    
            // only create / add the gradient layer once
            if gradLayer == nil {
    
                let gradientLayer = CAGradientLayer()
    
                gradientLayer.locations = [0, 1]
                gradientLayer.masksToBounds = true
    
                layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)
    
                self.gradLayer = gradientLayer
    
            }
    
            gradLayer?.colors = [startColor.cgColor, endColor.cgColor]
            gradLayer?.startPoint = CGPoint(x: 0.0, y: 0.0)
            gradLayer?.endPoint = CGPoint(x: 1.0, y: 0.6)
    
            gradLayer?.frame = bounds
            gradLayer?.cornerRadius = cornerRadius
    
    // original code
    //      let gradientLayer = CAGradientLayer()
    //      gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
    //      gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
    //      gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.6)
    //
    //      gradientLayer.locations = [0, 1]
    //      gradientLayer.frame = bounds
    //      gradientLayer.cornerRadius = cornerRadius
    //      gradientLayer.masksToBounds = true
    //
    //      layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)
        }
    
        override var isHighlighted: Bool {
            get {
                return super.isHighlighted
            }
            set {
                let xScale : CGFloat = newValue ? 1.025 : 1.0
                let yScale : CGFloat = newValue ? 1.1 : 1.0
                UIView.animate(withDuration: 0.1) {
                    let transformation = CGAffineTransform(scaleX: xScale, y: yScale)
                    self.transform = transformation
                }
    
                super.isHighlighted = newValue
            }
        }
    }