Codementor Events

How to draw a gradient border button?

Published Jan 08, 2019

Last week, my partner showed me his design for our application. Everything is great, easily implemented with some custom controls, except this.

A button with gradient border. Never try it before. Up to now, I just created gradient background views 2 times in previous projects. Googled and found some good results.

First solutions

let button = UIButton(frame: CGRect(x: 100, y: 100, width: 200, height: 100))
let gradient = CAGradientLayer()
gradient.frame =  CGRect(origin: .zero, size: button.frame.size)
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
let shape = CAShapeLayer()
shape.lineWidth = 2
shape.path = UIBezierPath(rect: button.bounds).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
button.layer.addSublayer(gradient)

It’s perfect.

Second solution

Rectangles with rounded corners are everywhere! (Steve Jobs)

Tried to make my button rounded corner. And…

Oops. It doesn't work as what I need. Did some more researches and found another great one from Ian Hirschfeld

It works like a charm, but it’s not my favorite solution. I need a Swifty project, not a combination with Objective-C. So don’t I try to make something simpler and easier for me?

My solution

The idea

  • Create a button with gradient background
  • Fill it with a solid color view
  • Set padding to the button bounds is the border width

Talk is cheap. Show me the code. (Linus Torvalds)

Setup gradient

func setupView() {    
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = bounds
    gradientLayer.colors = colors.map({ return $0.cgColor })
    gradientLayer.startPoint = startPoint
    gradientLayer.endPoint = endPoint
    layer.insertSublayer(gradientLayer, at: 0) // * important

    let backgroundView = UIView()
    backgroundView.translatesAutoresizingMaskIntoConstraints = false
    insertSubview(backgroundView, at: 1) // (1)
    backgroundView.backgroundColor = backgroundColor // (2)
    backgroundView.fill(toView: self, space: UIEdgeInsets(space: borderWidth)) // (3)
    createRoundCorner(cornerRadius)
}

(1)

  • The most important thing here is the order of the gradient layer and background view.
  • The title label will be overlapped and when we use addSublayer or addSubview.

(2)

  • backgroundColor: the view property from UIKit.

(3)

  • borderWidth is new property, we will use it every time re-setupView.

Add round corner

Now is time to create the rounded corner.

func createRoundCorner(_ radius: CGFloat) {
    cornerRadius = radius
    super.createRoundCorner(radius)
    backgroundView.createRoundCorner(radius - borderWidth)
}

Everytime we set the radius or backgroundColor, we call setupView. So that, the gradientLayer and backgroundView are added many times. Keep an instance and remove it everytime view setup.

Add borderWidth

But, no gradient border appears. The border width still zero. We need to give a non-zero value to borderWidth and setupView again.

func createBorder(_ width: CGFloat) {
    borderWidth = width
    setupView()
}

Result

Conclusion

Have a look at my completed code for this library at knGradientBorderButton
I use my auto layout library in this project. You can find it here: knConstraints

Feel free to comment, suggest, create pull request or open issue.

Enjoy coding.

Discover and read more posts from Ky Nguyen
get started
post commentsBe the first to share your opinion
Hafiz M Saad
2 years ago

First, thanks for such a nice tutorial as I couldn’t find much help in this regard. But will this work in TableView or CollectionView Cell as well? I tried, where I had auto-layout being used in the cells and the application started to crash.

Ky Nguyen
2 years ago

What is error you run into?

Hafiz M Saad
2 years ago

*** Terminating app due to uncaught exception ‘NSGenericException’, reason: ‘Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x600003ca0f80 "GradientBorderButton.knGradientBorderButton:0x7fc9cc206110’Button’.centerX"> and <NSLayoutXAxisAnchor:0x600003cac580 “UIView:0x7fc9cc0067d0.centerX”> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That’s illegal.’

Hafiz M Saad
2 years ago

My bad, I didn’t read the error. It was descriptive itself. Thanks

Bradford Yung
3 years ago

Instead, why not shape.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath instead? And, if you want the button’s text to be a gradient additionally, you could do gradient.mask = self.titleLabel?.layer with an appropriate font color for masking.

Ky Nguyen
3 years ago

Thanks for suggestions, @Bradford, didn’t think about it when i had this problem.

Show more replies