5

I have looked over a bunch of other answers to questions like this, but none of the answers seem to solve my problem, so looking for a little help.

I'm trying to apply a vertical gradient to a UIButton, but the gradient layer is just not showing up in my view. Here is the relevant code:

let darkPurple = UIColor(displayP3Red: 61.0/255.0, green: 3.0/255.0, blue: 110.0/255.0, alpha: 1.0)
let lightPurple = UIColor(displayP3Red: 90.0/255.0, green: 32.0/255.0, blue: 130.0/255.0, alpha: 1.0)

let addButton = UIButton(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
addButton.title = "Start counting"
addButton.layer.cornerRadius = 30.0
addButton.layer.borderWidth = 1.0
addButton.layer.borderColor = darkPurple.cgColor
addButton.translatesAutoresizingMaskIntoConstraints = false
myView.addSubview(addButton)
addButton.applyGradient(with: [lightPurple, darkPurple], gradientOrientation: .vertical)

...and the code to apply the gradient:

typealias GradientPoints = (startPoint: CGPoint, endPoint: CGPoint)

enum GradientOrientation {
  case topRightBottomLeft
  case topLeftBottomRight
  case horizontal
  case vertical

  var startPoint: CGPoint {
    return points.startPoint
  }

  var endPoint: CGPoint {
    return points.endPoint
  }

  var points: GradientPoints {
    switch self {
    case .topRightBottomLeft:
      return (CGPoint(x: 0.0, y: 1.0), CGPoint(x: 1.0, y: 0.0))
    case .topLeftBottomRight:
      return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y: 1.0))
    case .horizontal:
      return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y: 0.0))
    case .vertical:
      return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 0.0, y: 1.0))
    }
  }
}

extension UIView {
  func applyGradient(with colors: [UIColor], gradientOrientation orientation: GradientOrientation) {
    let gradient = CAGradientLayer()
    gradient.frame = self.bounds
    gradient.colors = colors.map { $0.cgColor }
    gradient.startPoint = orientation.startPoint
    gradient.endPoint = orientation.endPoint
    gradient.borderColor = self.layer.borderColor
    gradient.borderWidth = self.layer.borderWidth
    gradient.cornerRadius = self.layer.cornerRadius
    gradient.masksToBounds = true
    gradient.isHidden = false

    self.layer.insertSublayer(gradient, at: 0)
  }
}

This is what I get: empty button

Thanks for your help!

Duncan C
  • 128,072
  • 22
  • 173
  • 272
kyanring
  • 95
  • 1
  • 6

3 Answers3

16

For anyone who already spent hours of searching (like I did):

Make sure, that your colors array contains CGColor instead of UIColor, as per docs:

colors

An array of CGColorRef objects defining the color of each gradient stop. Animatable.

https://developer.apple.com/documentation/quartzcore/cagradientlayer/1462403-colors

Bonus: this little code snippet will crash app, if you miss it again (using #if DEBUG is recommended)

extension CAGradientLayer {
    dynamic var colors: [Any]? {
        get { self.value(forKey: "colors") as? [Any] }
        set {
            if let newValue = newValue {
                for color in newValue {
                    if color is UIColor {
                        fatalError("CAGradientLayer.colors must contain only CGColor! UIColor was found")
                    }
                }
            }
            super.setValue(newValue, forKey: "colors")
        }
    }
}
Dmytro Rostopira
  • 10,588
  • 4
  • 64
  • 86
3

Auto Layout doesn't affect the layers of UIView objects, so the gradient layer is being applied too early, before the frame of the UIButton has been calculated. When the frame property of the gradient layer is assigned, the bounds of the button are still equal to CGRect.zero, and the gradient layer frame is never updated later.

You'll find that if you add your gradient layer after the button frame has been calculated, it works as expected. For example:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    let darkPurple = UIColor(displayP3Red: 61.0/255.0, green: 3.0/255.0, blue: 110.0/255.0, alpha: 1.0)
    let lightPurple = UIColor(displayP3Red: 90.0/255.0, green: 32.0/255.0, blue: 130.0/255.0, alpha: 1.0)

    addButton.applyGradient(with: [lightPurple, darkPurple], gradientOrientation: .vertical)
}

enter image description here

Another option would be to update the frame property of the gradient layer in viewDidLayoutSubviews() or create a custom UIButton subclass and update the gradient layer frame whenever the frame of the button updates.

Reference

ryanecrist
  • 328
  • 2
  • 9
0

Sublayers are in back-to-front order. Inserting a layer at index 0 will place it behind any other sublayers. I've never tried to do that with a button, but suspect it has at least 1 sublayer that is covering the gradient you are trying to add.

Try using addSublayer() instead, which will put the layer on the top of the sublayers array.

Edit:

I built a variant of your code and when I change self.layer.insertSublayer(_:at:) to addSublayer(), it works.

Community
  • 1
  • 1
Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • You're right about this, for sure. In my case, I needed to make sure the gradient shows up behind the button title, so `self.layer.insertSublayer(_:at:)` won't work for me. I did, however, have this backward, thinking that 0 would be the top. Thanks for the correction! – kyanring Nov 20 '18 at 05:00