2

I am using the following code to apply a linear gradient to an iOs Button

 private func applyGradient(colors: [CGColor])
{
    let gradientLayer = CAGradientLayer()
    gradientLayer.colors = colors
    gradientLayer.startPoint = CGPoint(x: 0, y: 1)
    gradientLayer.endPoint = CGPoint(x: 0, y: 0)
    gradientLayer.frame = self.addToCart.bounds 
    self.addToCart.layer.insertSublayer(gradientLayer, at: 0)
}

However the gradient is not fully applied to the button. Here is an image of the ios Button

enter image description here

Ahmed
  • 1,229
  • 2
  • 20
  • 45

3 Answers3

2

An alternative to manually updating the frame of the gradient layer is to declare the button’s layerClass to be a CAGradientLayer:

@IBDesignable
public class GradientButton: UIButton {
    public override class var layerClass: AnyClass         { CAGradientLayer.self }
    private var gradientLayer: CAGradientLayer             { layer as! CAGradientLayer }

    @IBInspectable public var startColor: UIColor = .white { didSet { updateColors() } }
    @IBInspectable public var endColor: UIColor = .red     { didSet { updateColors() } }

    // init methods

    public override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        updateColors()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        updateColors()
    }
}

private extension GradientButton {
    func updateColors() {
        gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
    }
}

This achieves the behavior of the proposed solution, but also:

  • Unlike the manual setting of the gradient layer’s frame in the view controller’s viewDidLayoutSubviews (or, better, the button’s layoutSubviews), this will respond to animated layout changes more gracefully. If an animation of the button’s frame is underway (e.g. rotating the device or what have you), this layerClass approach will render the button correctly mid-animation. The manual setting of the gradient layer’s frame won’t.

    enter image description here

  • It is not necessary, but I have made this @IBDesignable, so should you add this button in IB, you can adjust the colors and the startPoint/endPoint right in Interface Builder, seeing it rendered realtime.

    enter image description here

    Here I made the gradient diagonal, changed the colors, and added a border, all from Interface Builder using this expanded rendition of the above.

    @IBDesignable
    public class GradientButton: UIButton {
        public override class var layerClass: AnyClass         { CAGradientLayer.self }
        private var gradientLayer: CAGradientLayer             { layer as! CAGradientLayer }
    
        @IBInspectable public var startColor: UIColor = .white { didSet { updateColors() } }
        @IBInspectable public var endColor: UIColor = .red     { didSet { updateColors() } }
    
        // expose startPoint and endPoint to IB
    
        @IBInspectable public var startPoint: CGPoint {
            get { gradientLayer.startPoint }
            set { gradientLayer.startPoint = newValue }
        }
    
        @IBInspectable public var endPoint: CGPoint {
            get { gradientLayer.endPoint }
            set { gradientLayer.endPoint = newValue }
        }
    
        // while we're at it, let's expose a few more layer properties so we can easily adjust them in IB
    
        @IBInspectable public var cornerRadius: CGFloat {
            get { layer.cornerRadius }
            set { layer.cornerRadius = newValue }
        }
    
        @IBInspectable public var borderWidth: CGFloat {
            get { layer.borderWidth }
            set { layer.borderWidth = newValue }
        }
    
        @IBInspectable public var borderColor: UIColor? {
            get { layer.borderColor.flatMap { UIColor(cgColor: $0) } }
            set { layer.borderColor = newValue?.cgColor }
        }
    
        // init methods
    
        public override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            updateColors()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            updateColors()
        }
    }
    
    private extension GradientButton {
        func updateColors() {
            gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
        }
    }
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

Usually, viewDidLoad for view controllers or init for custom views is too early for gradient frames. Also, their frames will most likely change later on, which you need to handle.

If you are applying a gradient to a custom view, try updating its frame inside layoutSubviews() (from this great answer).

class GradientButton: UIButton {

    /// update inside layoutSubviews
    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }

    private lazy var gradientLayer: CAGradientLayer = {
        let l = CAGradientLayer()
        l.frame = self.bounds
        l.colors = [UIColor.systemYellow.cgColor, UIColor.systemPink.cgColor]
        l.startPoint = CGPoint(x: 0, y: 1)
        l.endPoint = CGPoint(x: 0, y: )
        layer.insertSublayer(l, at: 0)
        return l
    }()
}

For a view in a view controller, try viewDidLayoutSubviews().

class ViewController: UIViewController {

    @IBOutlet weak var addToCart: UIButton!
    var gradientLayer: CAGradientLayer? /// keep a reference to the gradient layer so we can update its frame later  
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /// still first make the gradient inside viewDidLoad
        applyGradient(colors: [UIColor.systemYellow.cgColor, UIColor.systemPink.cgColor])
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        /// update here!
        self.gradientLayer?.frame = self.addToCart.bounds 
    }  

    private func applyGradient(colors: [CGColor]) {
        let gradientLayer = CAGradientLayer()
        gradientLayer.colors = colors
        gradientLayer.startPoint = CGPoint(x: 0, y: 1)
        gradientLayer.endPoint = CGPoint(x: 0, y: 0)
        gradientLayer.frame = self.addToCart.bounds 
        self.addToCart.layer.insertSublayer(gradientLayer, at: 0)

        self.gradientLayer = gradientLayer
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
  • 1
    If you have a button subclass with gradient, you probably should just update the gradient `frame` in the [`layoutSubviews`](https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews) of this subclass rather than the view controller’s `viewDidLayoutSubviews`. – Rob Mar 21 '21 at 19:09
  • 1
    @Rob yep, good point. Also voted for your answer – aheze Mar 26 '21 at 20:19
0

Try replacing these two lines:

    gradientLayer.startPoint = CGPoint(x: 0, y: 1)
    gradientLayer.endPoint = CGPoint(x: 0, y: 0)

With this one line:

gradientLayer.locations = [0.0, 1.0]
Travis
  • 207
  • 1
  • 11