How to draw a gradient border button?
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
oraddSubview
.
(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, thegradientLayer
andbackgroundView
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.
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.
What is error you run into?
*** 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.’
My bad, I didn’t read the error. It was descriptive itself. Thanks
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 dogradient.mask = self.titleLabel?.layer
with an appropriate font color for masking.Thanks for suggestions, @Bradford, didn’t think about it when i had this problem.