UIButton with gradient border and rounded corners - ios

What I am trying to get is the a custom UIButton that has a gradient border (just the border is gradient) and rounded corners. I almost got to where I wanted to be, but having issues with the corners.
Here is what I currently have:
Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: self.myButton.frame.size)
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.cornerRadius = 15
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.path = UIBezierPath(rect: self.myButton.bounds).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.cornerRadius = 15
gradient.mask = shape
self.myButton.clipsToBounds = true
self.myButton.layer.cornerRadius = 15
self.myButton.layer.addSublayer(gradient)
}
So the question is how do I display corners with gradient?

The problem is with the mask, try to remove shape.cornerRadius = 15 and change this:
shape.path = UIBezierPath(rect: self.myButton.bounds).cgPath
to this:
shape.path = UIBezierPath(roundedRect: self.myButton.bounds.insetBy(dx: 2.5, dy: 2.5), cornerRadius: 15.0).cgPath
Play with the values of the insets and cornerRadius to achieve your desired result.

Related

How to create Gradient border with rounded corners at UIView in Swift 4

I want to set border color with the gradient left to right [Red, Green] at UIView.
Like example:
I tried below code: -
class View: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: frame.size.height / 2, height: frame.size.height / 2))
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: frame.size)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
let shape = CAShapeLayer()
shape.lineWidth = 10
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
layer.insertSublayer(gradient, at: 0)
}
}
There is three problem which I am not able to resolve: -
1- I have set lineWidth 10 but its showing width 10 at corners and at horizontal/vertical only 5.
2- I want to show the gradient from left to right not top to bottom.
I tried below code to set gradient from left to right but not working: -
// gradient.frame = CGRect(origin: CGPoint.zero, size: frame.size)
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
Please help. Thanks in advance.
Edit
I want to set border from left to right
You need to change gradient.startPoint and gradient.endPoint
enum Direction {
case horizontal
case vertical
}
class View: UIView {
init(frame: CGRect, cornerRadius: CGFloat, colors: [UIColor], lineWidth: CGFloat = 5, direction: Direction = .horizontal) {
super.init(frame: frame)
self.layer.cornerRadius = cornerRadius
self.layer.masksToBounds = true
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: self.frame.size)
gradient.colors = colors.map({ (color) -> CGColor in
color.cgColor
})
switch direction {
case .horizontal:
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 1)
case .vertical:
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 0, y: 1)
}
let shape = CAShapeLayer()
shape.lineWidth = lineWidth
shape.path = UIBezierPath(roundedRect: self.bounds.insetBy(dx: lineWidth,
dy: lineWidth), cornerRadius: cornerRadius).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
self.layer.addSublayer(gradient)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
As you can see, I added some extra parameters. I solved this issue by adding inset to the roundedRect:
shape.path = UIBezierPath(roundedRect: self.bounds.insetBy(dx: lineWidth,
dy: lineWidth), cornerRadius: cornerRadius).cgPath
How to use it:
let myView = View(frame: CGRect(x: 0, y: 0, width: 200, height: 50), cornerRadius: 25, colors: [UIColor.red, .orange, .yellow], lineWidth: 2, direction: .horizontal)
myView.center = view.center
view.addSubview(myView)
Screenshot:
roundedViewWithGradient
The problem is that the the bezier path is not drawn inside its bounds. It's drawn around its bounds. So half the stroke width is inside and half is outside. You need to adjust the bounds to deal with this.
Change the frame you pass into the bezier path from self.bounds to self.bounds.insetBy(dx: 5, dy: 5) where 5 is half your line width.
And the lines:
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
do result in the gradient going from left to right.
Here is fully working code from yours:
class View: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(roundedRect: self.bounds.insetBy(dx: 5, dy: 5), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: frame.size.height / 2, height: frame.size.height / 2))
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: frame.size)
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
let shape = CAShapeLayer()
shape.lineWidth = 10
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
layer.insertSublayer(gradient, at: 0)
}
}

How to set UITextField border with gradient from left to right with rounded corners using Swift 4

I am trying to implement a UITextField activity with rounded gradient border from left to right like below sample: -
Requirement:
Here I have 2 text fields. On load view controller 'Email' will be active with the gradient border. When user resigns 'Email' field then the border will change to white color and again if user click in 'Email' text field then the border will change to the gradient.
I tried below code but not working properly: -
class CustomTextField: UITextField {
var gradient:CAGradientLayer! = nil
func canBecomeFirstResponder() -> Bool {
return true
}
override func becomeFirstResponder() -> Bool {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: frame.size.height / 2, height: frame.size.height / 2))
gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: frame.size)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
let shape = CAShapeLayer()
shape.lineWidth = 10
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
layer.addSublayer(gradient)
super.becomeFirstResponder()
return true
}
override func resignFirstResponder() -> Bool {
gradient.colors = [UIColor.white.cgColor]
super.resignFirstResponder()
return true
}
}
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var txtEmail: CustomTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
txtEmail.resignFirstResponder()
return true
}
}
There is three problem which I am not able to resolve: -
1- I have set lineWidth 10 but its showing width 10 at corners and at horizontal/vertical only 5.
2- I want to show the gradient from left to right not top to bottom.
3- When I resign, it is not setting white border
Please help, thanks in advance.
The line is stroked centrally to the path, so 10 pixel width means 5 pixels either side. Your shape is being clipped at the sides, so you need to inset the path.
Problem 1
let lineWidth: CGFloat = 10
let rect = bounds.inset.bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath(roundedRect: rect, cornerRadius: frame.size.height / 2)
shape.lineWidth = lineWidth
Alternatively, if you want the line to be stroked centrally on the path, you'll need to set your CustomTextField's clipsToBounds = false
Problem 2
To change the angle of the gradient, use the startPoint and endPoint
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
Problem 3
Possibly try
gradient.colors = [UIColor.white.cgColor, UIColor.white.cgColor]
First select "NO Border in XCode's element inspector"
then use below code
let lineWidth: CGFloat = 1
let rect = myTextField.bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath(roundedRect: rect, cornerRadius: myTextField.frame.size.height / 2)
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: myTextField.frame.size)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
let shape = CAShapeLayer()
shape.lineWidth = lineWidth
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
myTextField.layer.addSublayer(gradient)
P.S. This answer is fully inspired by #Ashley's answer.

CAGradientLayer is not getting the shape

I am creating a CAGradientLayer with the shape of a horseshoe. It seems it's not taking the path and it just draws a square with shadow. What am I missing ?
let gradientLayerContainer = CAShapeLayer()
let gradient = CAGradientLayer()
let segmentGradientPath = UIBezierPath.horseshoe(
center: centerPoint,
innerRadius: (bounds.width / 2) - config.ringWidth,
outerRadius: bounds.width / 2,
startAngle: segmentTapped.startAngle,
endAngle: segmentTapped.endAngle)
gradientLayerContainer.fillColor = UIColor.clear.cgColor
gradientLayerContainer.path = segmentGradientPath.cgPath
let end = centerPoint
let start = centerPoint.shifted(outerRadius, with: segmentTapped.centerAngle)
gradient.startPoint = CGPoint(x: start.x / bounds.width, y: start.y / bounds.height)
gradient.endPoint = CGPoint(x: end.x / bounds.width, y: end.y / bounds.height)
gradient.colors = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor]
gradient.frame = segmentGradientPath.bounds
gradient.mask = gradientLayerContainer
gradient.locations = [0, 1]
horseshoeLayer.addSublayer(gradientLayerContainer)
gradientLayerContainer.insertSublayer(gradient, at: 0)
The mask belongs to the gradient layer. The gradient layer does not belong to the mask. Also the mask color needs to be opaque everywhere you want the gradient to show (you are using clear which is the opposite).
Here is a Playground example of the correct hierarchy:
import PlaygroundSupport
import UIKit
let rect = CGRect(x: 0, y: 0, width: 300, height: 300)
let gradient = CAGradientLayer()
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(ovalIn: rect)
maskLayer.fillColor = UIColor.black.cgColor
maskLayer.path = maskPath.cgPath
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.mask = maskLayer
gradient.locations = [0, 1]
let view = UIView(frame: rect)
view.layer.addSublayer(gradient)
gradient.frame = view.bounds
PlaygroundPage.current.liveView = view

How to fill a bezier path with gradient color

I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect) function. I would like to fill the path with a gradient color. Please can anybody guide me how can I do that.
I need to fill the clip with a gradient color and then stroke the path with black color.
There are some posts in SO which does not solve the problem. For example Swift: Gradient along a bezier path (using CALayers) this post guides how to draw on a layer in UIView but not in a UIBezierPath.
NB: I am working on Swift-3
To answer this question of yours,
I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect)
function. I would like to fill the path with a gradient color.
Lets say you have an oval path,
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
To create a gradient,
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
We need a mask layer for gradient,
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
Now set this shapeLayer as mask of gradient layer and add it to view's layer as subLayer
gradient.mask = shapeMask
yourCustomView.layer.addSublayer(gradient)
Update
Create a base layer with stroke and add before creating gradient layer.
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.view.layer.addSublayer(shape)
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
self.view.layer.addSublayer(gradient)
You can do this directly in Core Graphics without using CALayer classes. Use bezierPath.addClip() to set the bezier path as the clipping region. Any subsequent drawing commands will be masked to that region.
I use this wrapper function in one of my projects:
func drawLinearGradient(inside path:UIBezierPath, start:CGPoint, end:CGPoint, colors:[UIColor])
{
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
defer { ctx.restoreGState() } // clean up graphics state changes when the method returns
path.addClip() // use the path as the clipping region
let cgColors = colors.map({ $0.cgColor })
guard let gradient = CGGradient(colorsSpace: nil, colors: cgColors as CFArray, locations: nil)
else { return }
ctx.drawLinearGradient(gradient, start: start, end: end, options: [])
}
The other answers here don't seem to work (At least in newer iOS versions?)
Here's a working example of a bezier path being created and the stroke being a gradient stroke, adjust the CGPoints and CGColors as needed:
CGPoint start = CGPointMake(300, 400);
CGPoint end = CGPointMake(350, 400);
CGPoint control = CGPointMake(300, 365);
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:start];
[path addQuadCurveToPoint:end controlPoint:control];
CAShapeLayer *shapeLayer = [CAShapeLayer new];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 2.0;
//[self.view.layer addSublayer:shapeLayer];
CGRect gradientBounds = CGRectMake(path.bounds.origin.x-shapeLayer.lineWidth, path.bounds.origin.y-shapeLayer.lineWidth, path.bounds.size.width+shapeLayer.lineWidth*2.0, path.bounds.size.height+shapeLayer.lineWidth*2.0);
CAGradientLayer *gradientLayer = [CAGradientLayer new];
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = #[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
gradientLayer.mask = shapeLayer;
[self.view.layer addSublayer:gradientLayer];
Swift 5 version of #AlbertRenshaw's (ObjC) answer, and wrapped in a UIView class.
What it displays looks spherical but on it's side.
A CGAffineTransform could be applied to rotate it 90ยบ for a cool effect.
import UIKit
class SphereView : UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let centerPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
let radius = frame.height / 2
let circlePath = UIBezierPath(arcCenter: centerPoint,
radius: radius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath;
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2.0;
let gradientBounds = CGRectMake(
circlePath.bounds.origin.x - shapeLayer.lineWidth,
circlePath.bounds.origin.y - shapeLayer.lineWidth,
circlePath.bounds.size.width + shapeLayer.lineWidth * 2.0,
circlePath.bounds.size.height + shapeLayer.lineWidth * 2.0)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = [UIColor.red, UIColor.blue].map { $0.cgColor }
gradientLayer.mask = shapeLayer;
self.layer.addSublayer(gradientLayer)
}
}
Usage in a UIViewController:
override func viewDidLoad() {
view.backgroundColor = .white
let sphereView = SphereView()
sphereView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sphereView)
NSLayoutConstraint.activate([
sphereView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
sphereView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
sphereView.widthAnchor.constraint(equalToConstant: 150),
sphereView.heightAnchor.constraint(equalToConstant: 150),
])
}

How to set gradient color to UIImageView border

How to apply gradient color to UIImageView border color. What i tried is
let gradient: CAGradientLayer = CAGradientLayer()
let color0 = UIColor(red:0.0/255, green:0.0/255, blue:0.0/255, alpha:0.0).CGColor
let color1 = UIColor(red:0.0/255, green:0.0/255, blue: 0.0/255, alpha:0.71).CGColor
gradient.colors = [color0, color1]
gradient.locations = [0.0 , 1.0]
gradient.frame = CGRect(x: 0.0, y: 80.0, width: self.imgProfile.frame.size.width, height: self.imgProfile.frame.size.height-35)
self.imgProfile.layer.insertSublayer(gradient, atIndex: 0)
This is what required
Please guide, thanks
Update:
Tried this way
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPointZero, size: self.imgPeopleProfile.frame.size)
gradient.colors = [UIColor.blueColor().CGColor, UIColor.greenColor().CGColor]
let shape = CAShapeLayer()
shape.lineWidth = 2
shape.path = UIBezierPath(rect: self.imgPeopleProfile.bounds).CGPath
shape.strokeColor = UIColor.blackColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
gradient.mask = shape
imgPeopleProfile.layer.cornerRadius = imgPeopleProfile.frame.size.width / 2
imgPeopleProfile.clipsToBounds = true
self.imgPeopleProfile.layer.addSublayer(gradient)
It gives
Please take Imageview Size Equal width and height ex. Width=100 and Height = 100 So it will show proper Circle. If you want to change the Size then please amke also change in the below Line .
shape.path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: CGFloat(50), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true).CGPath
With the radius of the Circle in above example its 50.
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPointZero, size: self.imageView.frame.size)
gradient.colors = [UIColor(red:87.0/255, green:206.0/255, blue: 172.0/255, alpha:0.71).CGColor,UIColor(red:44.0/255, green:192.0/255, blue:208.0/255, alpha:1.0).CGColor]
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: CGFloat(50), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true).CGPath
shape.strokeColor = UIColor.blackColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
gradient.mask = shape
self.imageView.layer.addSublayer(gradient)

Resources