I have a vertical progress bar with an animating CAGradientLayer that shows "activity" to the user.
My problem is I can't get the animation to run top-down where the gradient line is parallel to x-axis. It currently animates left to right with the gradient line parallel to the y-axis.
I thought by adjusting the layer's startPoint and endPoint y-value it would do the trick, but the layer continues to animate from left to right.
Any guidance would be appreciated.
class ProgressBarView: UIView {
var color: UIColor = .red {
didSet { setNeedsDisplay() }
}
var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
var progress: CGFloat = 0 {
didSet {
DispatchQueue.main.async { self.setNeedsDisplay() }
}
}
private let progressLayer = CALayer()
private let gradientLayer = CAGradientLayer()
private let backgroundMask = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
createAnimation()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
createAnimation()
}
override func draw(_ rect: CGRect) {
self.backgroundColor = UIColor.lightGray
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: 0.5, y: progress)
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: 8).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(x: 0, y: rect.height, width: rect.width, height: -(rect.height - (rect.height * progress)))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
}
private func setupLayers() {
layer.addSublayer(gradientLayer)
gradientLayer.mask = progressLayer
gradientLayer.locations = [0.35, 0.5, 0.65]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
}
private func createAnimation() {
let flowAnimation = CABasicAnimation(keyPath: "locations")
flowAnimation.fromValue = [-0.3, -0.15, 0]
flowAnimation.toValue = [1, 1.15, 1.3]
flowAnimation.isRemovedOnCompletion = false
flowAnimation.repeatCount = Float.infinity
flowAnimation.duration = 1
gradientLayer.add(flowAnimation, forKey: "flowAnimation")
}
}
This should get you started...
On init:
// add the gradient layer
layer.addSublayer(gradientLayer)
// initial locations
gradientLayer.locations = [0.35, 0.5, 0.65]
// initial colors
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
// set start and end points
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
// set the mask
gradientLayer.mask = backgroundMask
Don't override draw() ... instead, in layoutSubviews():
var r = bounds
// make gradient layer progress % of height
r.size.height *= progress
// update the mask path
backgroundMask.path = UIBezierPath(roundedRect: r, cornerRadius: 8).cgPath
// update gradient layer frame
gradientLayer.frame = r
When you update the progress property, call setNeedsLayout() to update the layer frames.
Here's a modified version of your class:
class ProgressBarView: UIView {
var color: UIColor = .red {
didSet {
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
}
}
var gradientColor: UIColor = .white {
didSet {
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
}
}
var progress: CGFloat = 0 {
didSet {
// trigger layoutSubviews() to
// update the layer frames
setNeedsLayout()
}
}
private let gradientLayer = CAGradientLayer()
private let backgroundMask = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.backgroundColor = UIColor.lightGray
setupLayers()
createAnimation()
}
private func setupLayers() {
// add the gradient layer
layer.addSublayer(gradientLayer)
// initial locations
gradientLayer.locations = [0.35, 0.5, 0.65]
// initial colors
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
// set start and end points
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
// set the mask
gradientLayer.mask = backgroundMask
}
private func createAnimation() {
let flowAnimation = CABasicAnimation(keyPath: "locations")
flowAnimation.fromValue = [-0.3, -0.15, 0]
flowAnimation.toValue = [1, 1.15, 1.3]
flowAnimation.isRemovedOnCompletion = false
flowAnimation.repeatCount = Float.infinity
flowAnimation.duration = 1
gradientLayer.add(flowAnimation, forKey: "flowAnimation")
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// make gradient layer progress % of height
r.size.height *= progress
// update the mask path
backgroundMask.path = UIBezierPath(roundedRect: r, cornerRadius: 8).cgPath
// update gradient layer frame
gradientLayer.frame = r
}
}
and an example controller. Progress will start at 5% and increment by 10% with each tap anywhere on the screen:
class ViewController: UIViewController {
let pbView = ProgressBarView()
let infoLabel = UILabel()
var progress: CGFloat = 0.05
override func viewDidLoad() {
super.viewDidLoad()
pbView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pbView)
infoLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(infoLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
pbView.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
pbView.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
pbView.widthAnchor.constraint(equalToConstant: 60.0),
pbView.heightAnchor.constraint(equalToConstant: 400.0),
infoLabel.topAnchor.constraint(equalTo: pbView.bottomAnchor, constant: 20.0),
infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
infoLabel.font = .systemFont(ofSize: 32.0, weight: .light)
pbView.progress = self.progress
updateInfo()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
progress += 0.1
pbView.progress = min(1.0, progress)
updateInfo()
}
func updateInfo() {
let pInt = Int(progress * 100.0)
infoLabel.text = "\(pInt)%"
}
}
("Percent Label" value can be off due to rounding.)
Edit - to clarify why it wasn't working..
So, why wasn't it working to begin with?
The original code was changing the gradient layer's .endPoint to a percentage of the height.
However, the .locations are percentages of the .endPoint - .startPoint value.
Suppose the view is 400-pts tall... if we're at 25% and we set:
.startPoint = CGPoint(x: 0.5, y: 0.0)
.endPoint = CGPoint(x: 0.5, y: 0.25)
the gradient will be calculated for 100-pts of height.
The .locations animation goes from [-0.3, -0.15, 0] to [1, 1.15, 1.3] 0 which is a total of 30% of the 100-pts (or 30-points). However, as soon as the location exceeds 1.0 it will fill out the rest of the layer's frame.
Here's how it looks as we animate through:
gradientLayer.locations = [0.10, 0.25, 0.4]
gradientLayer.locations = [0.40, 0.55, 0.7]
gradientLayer.locations = [0.60, 0.75, 0.9]
gradientLayer.locations = [0.85, 1.0, 1.15]
I've adjusted the gray "progress" layer to be only half of the width -- at full width, it covers the beginning of the gradient animation:
Setting aside any discussion of putting the code inside draw() or layoutSubviews(), you can "fix" the issue by commenting out a single line in your draw() func:
//gradientLayer.endPoint = CGPoint(x: 0.5, y: progress)
Now the actual gradient height will remain at 30% of the full height.
It wasn't clear initially what you wanted to do with the gray "progress" layer... here's a modified version using layoutSubviews() instead of draw(). One big benefit is that the entire thing will automatically resize if the view frame changes:
class ProgressBarView: UIView {
var color: UIColor = .red {
didSet {
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
}
}
var gradientColor: UIColor = .white {
didSet {
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
}
}
var progress: CGFloat = 0 {
didSet {
// trigger layoutSubviews() to
// update the layer frames
setNeedsLayout()
}
}
private let gradientLayer = CAGradientLayer()
private let progressLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.backgroundColor = UIColor.lightGray
setupLayers()
createAnimation()
// give the full view rounded corners
self.layer.cornerRadius = 8
self.layer.masksToBounds = true
}
private func setupLayers() {
// initial locations
gradientLayer.locations = [0.35, 0.5, 0.65]
// initial colors
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
// set start and end points
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
// add the gradient layer
layer.addSublayer(gradientLayer)
// add the gray "progress" layer
progressLayer.backgroundColor = UIColor.lightGray.cgColor
layer.addSublayer(progressLayer)
}
private func createAnimation() {
let flowAnimation = CABasicAnimation(keyPath: "locations")
flowAnimation.fromValue = [-0.3, -0.15, 0]
flowAnimation.toValue = [1, 1.15, 1.3]
flowAnimation.isRemovedOnCompletion = false
flowAnimation.repeatCount = Float.infinity
flowAnimation.duration = 1
gradientLayer.add(flowAnimation, forKey: "flowAnimation")
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// update gradient layer frame
gradientLayer.frame = bounds
// make gray progress layer frame height % of height
r.size.height *= progress
// update the gray progress layer frame
progressLayer.frame = r
}
}
I created a UICollectionView with cell like a credit cards and want to add gradient layer as a background of this cards. First I added background view for shadow(shadowView) and then add second view for gradient layer(bgView) and add CAGradientLayer as subLayer of bgView. But I didn't get any gradient view. This is my code:
class CardCell: BaseCell {
private var shadowView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.white
view.layer.shadowColor = UIColor.shadowColor.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 1.0)
view.layer.shadowOpacity = 0.1
view.layer.shadowRadius = 15.0
return view
}()
private var bgView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
view.backgroundColor = .clear
view.layer.addSublayer(createGradientLayer(for: view))
return view
}()
override func setup() {
super.setup()
addSubview(shadowView)
addSubview(bgView)
// setup constraints
shadowView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.9).isActive = true
shadowView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1).isActive = true
shadowView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
shadowView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
bgView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true
bgView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor).isActive = true
bgView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor).isActive = true
}
private static func createGradientLayer(for view: UIView) -> CAGradientLayer {
let color = UIColor(red:0.95, green:0.42, blue:0.64, alpha:1.0)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = view.bounds
gradientLayer.colors = [color.cgColor, color.withAlphaComponent(0.7).cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.1, y: 1.1)
return gradientLayer
}
}
But If I change this code and remove gradient setting from bgView init and write it after constraints, everything is OK, BUT the shadow view go away:
. . .
bgView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor).isActive = true
bgView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor).isActive = true
bgView.layoutIfNeeded()
bgView.layer.addSublayer(CardCell.createGradientLayer(for: bgView))
extension UIView {
func addGradientToViewWithCornerRadiusAndShadow(radius:CGFloat,firstColor:UIColor,secondColor:UIColor,locations:[CGFloat]){
for layer in (self.layer.sublayers ?? []){
if let layer1 = layer as? CAGradientLayer{
layer1.removeFromSuperlayer()
}
}
let gradient = CAGradientLayer()
var rect = self.bounds
rect.size.width = ScreenWidth
gradient.frame = rect
gradient.colors = [firstColor.cgColor, secondColor.cgColor]
gradient.locations = locations as [NSNumber]
gradient.startPoint = CGPoint.init(x: 0, y: 0)
gradient.endPoint = CGPoint.init(x: 0, y: 1)
gradient.cornerRadius = radius
gradient.shadowRadius = 2
gradient.shadowColor = UIColor.lightGray.cgColor
gradient.shadowOffset = CGSize.init(width: 0.5, height: 0.5)
gradient.shadowOpacity = 0.5
self.layer.insertSublayer(gradient, at: 0)
}
}
try using this UIView's extension.Let me know if this helps.
I think your gradient layer add on your shadow view show your shadow view disappear..You should always add gradient layer at 0 index of layers.
bgView.layer.insertSublayer(CardCell.createGradientLayer(for: bgView), at: 0)
and Your layer creation method you need to use frame instead bounds.
private static func createGradientLayer(for view: UIView) -> CAGradientLayer {
let color = UIColor(red:0.95, green:0.42, blue:0.64, alpha:1.0)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = view.frame
gradientLayer.colors = [color.cgColor, color.withAlphaComponent(0.7).cgColor]
gradientLayer.locations = nil
return gradientLayer
}
The problem is When you add the gradient view the bgView is not layout completely. So the bound will be zero.
Solution:
private var gradient: CAGradientLayer?
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath
if let gradient = gradient {
gradient.frame = bgView.bounds
} else {
gradient = TestCell.createGradientLayer(for: bgView)
bgView.layer.addSublayer(gradient!)
}
}
OUTPUT:
I'm trying to draw some dashed lines on an app but it only draws on main.storyboard with IBDesignable. When I run the app on iOS simulator, nothing shows. What's happening?
The code to draw:
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord = 50.0
#IBInspectable var thinYCoord = 52.5
override init(frame: CGRect) {
super.init(frame: frame)
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
//thickHorizontalLayer.frame = frame
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
//thickHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
//thinHorizontalLayer.frame = frame
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
//thinHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thinHorizontalLayer)
You need to put the common code in a routine that is called by both init(frame:) and init(coder:):
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0
#IBInspectable var thinYCoord: CGFloat = 52.5
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
self.layer.addSublayer(thinHorizontalLayer)
}
}
I'd also suggest declaring an explicit type for your #IBInspectable types, or else you won't be able to adjust them in IB.
Personally, rather than hard coding the path width, I'd update it when the layout changes. Also, if you're going to make those properties #IBDesignable, you really want to update the paths if they change.
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0 { didSet { updatePaths() } }
#IBInspectable var thinYCoord: CGFloat = 52.5 { didSet { updatePaths() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(thickHorizontalLayer)
layer.addSublayer(thinHorizontalLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
private func updatePaths() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [1.0, NSNumber(value: Double(bounds.size.width - 1) / 4 - 1.0) ]
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [0.5, NSNumber(value: Double(bounds.size.width - 1) / 40 - 0.5)]
}
}
You might want to adjust the dashing to span the width, too (I'm not sure if you wanted a consistent scale or for it to span the width). But hopefully this illustrates the idea.
I have no idea how to set the background gradient on a button (without making the background gradient an image). This is so different from Android.
Here's a class I have to define a returnable gradient scheme:
import UIKit
extension CAGradientLayer {
func backgroundGradientColor() -> CAGradientLayer {
let topColor = UIColor(red: (0/255.0), green: (153/255.0), blue:(51/255.0), alpha: 1)
let bottomColor = UIColor(red: (0/255.0), green: (153/255.0), blue:(255/255.0), alpha: 1)
let gradientColors: [CGColor] = [topColor.CGColor, bottomColor.CGColor]
let gradientLocations: [Float] = [0.0, 1.0]
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
return gradientLayer
}
}
I can use this to set the background of my entire view with the following:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let background = CAGradientLayer().backgroundGradientColor()
background.frame = self.view.bounds
self.view.layer.insertSublayer(background, atIndex: 0)
}
//...
}
But how can I access the view of the button and insert the sublayer or something like that?
Your code works fine. You just have to remember to set the gradient's frame every time. It is better to just make the gradient category also set the frame of the view for you.
That way you don't forget and it applies fine.
import UIKit
extension UIView {
func applyGradient(colours: [UIColor]) -> CAGradientLayer {
return self.applyGradient(colours: colours, locations: nil)
}
func applyGradient(colours: [UIColor], locations: [NSNumber]?) -> CAGradientLayer {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.locations = locations
self.layer.insertSublayer(gradient, at: 0)
return gradient
}
}
class ViewController: UIViewController {
#IBOutlet weak var btn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.btn.applyGradient(colours: [.yellow, .blue])
self.view.applyGradient(colours: [.yellow, .blue, .red], locations: [0.0, 0.5, 1.0])
}
}
Buttons are views. You apply gradients to it the same way you would apply it to any other view.
Picture Proof:
Video Proof:
https://i.imgur.com/ssDTqPu.mp4
It's this simple:
import UIKit
class ActualGradientButton: UIButton {
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: 0.5)
l.endPoint = CGPoint(x: 1, y: 0.5)
l.cornerRadius = 16
layer.insertSublayer(l, at: 0)
return l
}()
}
Here below you can find the solution for Swift3 (and Swift4 too) and a little bit extended (orientation helper):
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,y: 1))
case .horizontal:
return (CGPoint(x: 0.0,y: 0.5), CGPoint(x: 1.0,y: 0.5))
case .vertical:
return (CGPoint(x: 0.0,y: 0.0), CGPoint(x: 0.0,y: 1.0))
}
}
}
extension UIView {
func applyGradient(with colours: [UIColor], locations: [NSNumber]? = nil) {
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.locations = locations
self.layer.insertSublayer(gradient, at: 0)
}
func applyGradient(with colours: [UIColor], gradient orientation: GradientOrientation) {
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = orientation.startPoint
gradient.endPoint = orientation.endPoint
self.layer.insertSublayer(gradient, at: 0)
}
}
#Zeb answer is great but just to clean it up and make it a little more swifty.
Computed read-only properties should avoid using get and returning Void is redundant:
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, y: 1))
case .horizontal:
return (CGPoint(x: 0.0, y: 0.5), CGPoint(x: 1.0, y: 0.5))
case .vertical:
return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 0.0, y: 1.0))
}
}
}
extension UIView {
func applyGradient(withColours colours: [UIColor], locations: [NSNumber]? = nil) {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.locations = locations
self.layer.insertSublayer(gradient, at: 0)
}
func applyGradient(withColours colours: [UIColor], gradientOrientation orientation: GradientOrientation) {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = orientation.startPoint
gradient.endPoint = orientation.endPoint
self.layer.insertSublayer(gradient, at: 0)
}
}
If you want a gradient background on a button, rather than adding the gradient as a sublayer and changing its frame in layoutSubviews, I would instead just specify the layerClass of the button to be a CAGradientLayer, so the main layer is a gradient:
#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]
}
}
By setting the layerClass, it will just make the main layer be a gradient, which automatically is adjusted to the bounds of the button for you. This has an advantage that if you animate the changing of the button size (e.g. on rotation events or whatever), the gradient will be correctly animated, too.
And, it is not necessary, but it may be convenient to make this class an #IBDesignable, so one can set its properties in IB, and it will be correctly rendered in the storyboard/NIB with no additional code in the view controller. For example, I can customize the corners, border, and gradient colors and direction in IB:
Try this is working for me ,
let button = UIButton(frame: CGRect(x: 60, y: 150, width: 200, height: 60))
button.setTitle("Email", for: .normal)
button.backgroundColor = .red
button.setTitleColor(UIColor.black, for: .normal)
button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
// Apply Gradient Color
let gradientLayer:CAGradientLayer = CAGradientLayer()
gradientLayer.frame.size = button.frame.size
gradientLayer.colors =
[UIColor.white.cgColor,UIColor.green.withAlphaComponent(1).cgColor]
//Use diffrent colors
button.layer.addSublayer(gradientLayer)
self.view.addSubview(button)
You can add starting and end point of gradient color.
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
For more details description refer CAGradientLayer doc
I have tried all of them this is my button init inside of viewdidload
let button = UIButton()
button.setTitle("Alper", for: .normal)
button.layer.borderColor = UIColor.white.cgColor
button.layer.borderWidth = 1
view.addSubview(button)
button.anchor(top: nil, left: nil, bottom: logo.topAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, height: 50, width: 100)
let gradientx = CAGradientLayer()
gradientx.colors = [UIColor.blue,UIColor.red]
gradientx.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientx.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientx.frame = button.bounds
button.layer.insertSublayer(gradientx, at: 0)
anchor is an extension, so this is irrelevant gradient.
There are already many answers there I want add what I did to achieve this. I use this custom Button GradientButton
import Foundation
import UIKit
class GradientButton: UIButton {
let gradientColors : [UIColor]
let startPoint : CGPoint
let endPoint : CGPoint
required init(gradientColors: [UIColor] = [UIColor.red, UIColor.blue],
startPoint: CGPoint = CGPoint(x: 0, y: 0.5),
endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
self.gradientColors = gradientColors
self.startPoint = startPoint
self.endPoint = endPoint
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let halfOfButtonHeight = layer.frame.height / 2
contentEdgeInsets = UIEdgeInsets(top: 10, left: halfOfButtonHeight, bottom: 10, right: halfOfButtonHeight)
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
backgroundColor = UIColor.clear
// setup gradient
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = gradientColors.map { $0.cgColor }
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.cornerRadius = 4
// replace gradient as needed
if let oldGradient = layer.sublayers?[0] as? CAGradientLayer {
layer.replaceSublayer(oldGradient, with: gradient)
} else {
layer.insertSublayer(gradient, below: nil)
}
// setup shadow
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: halfOfButtonHeight).cgPath
layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
layer.shadowOpacity = 0.85
layer.shadowRadius = 4.0
}
override var isHighlighted: Bool {
didSet {
let newOpacity : Float = isHighlighted ? 0.6 : 0.85
let newRadius : CGFloat = isHighlighted ? 6.0 : 4.0
let shadowOpacityAnimation = CABasicAnimation()
shadowOpacityAnimation.keyPath = "shadowOpacity"
shadowOpacityAnimation.fromValue = layer.shadowOpacity
shadowOpacityAnimation.toValue = newOpacity
shadowOpacityAnimation.duration = 0.1
let shadowRadiusAnimation = CABasicAnimation()
shadowRadiusAnimation.keyPath = "shadowRadius"
shadowRadiusAnimation.fromValue = layer.shadowRadius
shadowRadiusAnimation.toValue = newRadius
shadowRadiusAnimation.duration = 0.1
layer.add(shadowOpacityAnimation, forKey: "shadowOpacity")
layer.add(shadowRadiusAnimation, forKey: "shadowRadius")
layer.shadowOpacity = newOpacity
layer.shadowRadius = newRadius
let xScale : CGFloat = isHighlighted ? 1.025 : 1.0
let yScale : CGFloat = isHighlighted ? 1.05 : 1.0
UIView.animate(withDuration: 0.1) {
let transformation = CGAffineTransform(scaleX: xScale, y: yScale)
self.transform = transformation
}
}
}
}
You can make GradientButton instance like this.
let button = GradientButton.init(gradientColors:[UIColor.black, UIColor.white], startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 0, y: 1))
For Swift
extension UIViewController {
func makeGradientColor(`for` object : AnyObject , startPoint : CGPoint , endPoint : CGPoint) -> CAGradientLayer {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [(UIColor.red.cgColor), (UIColor.yellow.cgColor)]
gradient.locations = [0.0 , 1.0]
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.frame = CGRect(x: 0.0, y: 0.0, width: object.bounds.size.width, height: object.bounds.size.height)
return gradient
}
}
How to use ?
if let layers = btn.layer.sublayers{
for layer in layers {
if layer.isKind(of: CAGradientLayer.self) {
layer.removeFromSuperlayer()
}
}
}
let start : CGPoint = CGPoint(x: 0.0, y: 0.0)
let end : CGPoint = CGPoint(x: 1.0, y: 1.0)
let gradient: CAGradientLayer = self.makeGradientColor(for: cell.bgView, startPoint: start, endPoint: end)
btn.layer.insertSublayer(gradient, at: 0)
I've modified this great answer to improve the reusability of the button by adding init parameters for colors, radius, and gradient direction.
I also added updateGradientColors method as it might be useful if you want to change the gradient color at some point.
class GradientButton: UIButton {
private let colors: [UIColor]
private let cornerRadius: CGFloat
private let startPoint: CGPoint
private let endPoint: CGPoint
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init(colors: [UIColor],
cornerRadius: CGFloat = 10,
startPoint: CGPoint = CGPoint(x: 0, y: 0.5),
endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
self.colors = colors
self.cornerRadius = cornerRadius
self.startPoint = startPoint
self.endPoint = endPoint
super.init(frame: .zero)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
private lazy var gradientLayer: CAGradientLayer = {
let gl = CAGradientLayer()
gl.frame = self.bounds
gl.colors = colors.map { $0.cgColor }
gl.startPoint = startPoint
gl.endPoint = endPoint
gl.cornerRadius = cornerRadius
layer.insertSublayer(gl, at: 0)
return gl
}()
func updateGradientColors(_ colors: [UIColor]) {
gradientLayer.colors = colors.map { $0.cgColor }
}
}
Gradient Button with corner radius, start and End Points Code is here...
extension UIView {
func applyGradient(colours: [UIColor], cornerRadius: CGFloat?, startPoint: CGPoint, endPoint: CGPoint) {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
if let cornerRadius = cornerRadius {
gradient.cornerRadius = cornerRadius
}
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.colors = colours.map { $0.cgColor }
self.layer.insertSublayer(gradient, at: 0)
}
}
Usage :
self.yourButton.applyGradient(colours: [.red, .green], cornerRadius: 20, startPoint: CGPoint(x: 0, y: 0.5), endPoint: CGPoint(x: 1, y: 0.5))
class ButtonGradient : UIButton {
override func layoutSubviews() {
let layer : CAGradientLayer = CAGradientLayer()
layer.frame.size = self.frame.size
layer.frame.origin = CGPoint(x: 0, y: 0)
// layer.cornerRadius = CGFloat(frame.width / 20)
let color0 = UIColor(red:255/255, green:122/255, blue:0/255, alpha:1.0).cgColor
let color1 = UIColor(red:255/255, green:176/255, blue: 0/255, alpha:1.0).cgColor
let color2 = UIColor(red:250/255, green:98/255, blue: 44/255, alpha:1.0).cgColor
layer.locations = [0.5, 1.0]
layer.startPoint = CGPoint(x: 0.0, y: 0.5)
layer.endPoint = CGPoint(x: 0.5, y: 0.5)
layer.colors = [color2,color0,color1]
self.layer.insertSublayer(layer, at: 0)
}
}
After that directly assign "ButtonGredient" class to particular button in Storyboard.
Here, I have taken one UIView and add button in it.
#IBOutlet weak var btnCenter: UIButton!
#IBOutlet weak var viewCenter: UIView!
// Create a gradient layer
let gradient = CAGradientLayer()
// gradient colors in order which they will visually appear
gradient.colors = [UIColor.yello.cgColor, UIColor.blue.cgColor]
// Gradient from left to right
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
// set the gradient layer to the same size as the view
gradient.frame = viewCenter.bounds
// add the gradient layer to the views layer for rendering
viewCenter.layer.insertSublayer(gradient, at: 0)
// Tha magic! Set the button as the views mask
viewCenter.mask = btnCenter
//Set corner Radius and border Width of button
btnCenter.layer.cornerRadius = btnCenter.frame.size.height / 2
btnCenter.layer.borderWidth = 5.0
There are ways to work with initial layer without making sublayers.
import UIKit
#IBDesignable class GradientButton: UIButton {
#IBInspectable var startColor: UIColor = UIColor.white
#IBInspectable var endColor: UIColor = UIColor.white
#IBInspectable var cornerRadius = CGFloat(5.0)
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
//This is an advanced gradient we do not use for now
// (layer as! CAGradientLayer).startPoint = CGPoint(x: 0, y: 0)
// (layer as! CAGradientLayer).endPoint = CGPoint(x: 1, y: 1)
// (layer as! CAGradientLayer).locations = [0,1]
// Simple gradient
(layer as! CAGradientLayer).colors = [startColor.cgColor, endColor.cgColor]
layer.cornerRadius = cornerRadius
}
}
class GradientButton: UIButton {
var gradientLayer: CAGradientLayer? {
didSet {
layer.sublayers?.filter { $0 is CAGradientLayer }.forEach { $0.removeFromSuperlayer() }
if let gradientLayer = gradientLayer {
layer.insertSublayer(gradientLayer, at: 0)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer?.frame = self.bounds
}
}