Custom UIButton's edges look pixelated after interacting with it - ios

I've created a custom UIButton subclass with rounded corners, gradient background and shadows. Buttons look ok and antialiased immediately after starting the application, but if I press one it's edges become pixelated.
I've tried a lot of stuff like setting .allowsEdgeAntialiasing = true on button's layers or removing scale transform animation from "Highlighted" setter etc. and nothing helps at all :(
Here is my button class:
#IBDesignable class CircleTintedButton: UIButton {
#IBInspectable var cornerRadius : CGFloat = 1.0
#IBInspectable var shadowOffsetWidth: CGFloat = 0.0
#IBInspectable var shadowOffsetHeight: CGFloat = 2.0
#IBInspectable var shadowColor : UIColor = UIColor.gray
#IBInspectable var shadowOpacity: CGFloat = 0.3
#IBInspectable var startColor: UIColor = .blue {
didSet {
setNeedsLayout()
}
}
#IBInspectable var endColor: UIColor = .green {
didSet {
setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = cornerRadius
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
layer.shadowPath = shadowPath.cgPath
layer.shadowOpacity = Float(shadowOpacity)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.6)
gradientLayer.locations = [0, 1]
gradientLayer.frame = bounds
gradientLayer.cornerRadius = cornerRadius
gradientLayer.masksToBounds = true
layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)
}
override var isHighlighted: Bool {
get {
return super.isHighlighted
}
set {
let xScale : CGFloat = newValue ? 1.025 : 1.0
let yScale : CGFloat = newValue ? 1.1 : 1.0
UIView.animate(withDuration: 0.1) {
let transformation = CGAffineTransform(scaleX: xScale, y: yScale)
self.transform = transformation
}
super.isHighlighted = newValue
}
}
}
Some screenshots from my test device (iPhone 7 # 12.1.2):
After app launch:
https://vinishko.party/files/ok.jpg
After I press this button:
https://vinishko.party/files/aliased.jpg
Spent the whole day already trying to fix this problem, please help me :D
Thanks.

You are adding another gradient layer every time layoutSubviews() is called. You can confirm this by adding an #IBAction for your button:
#IBAction func didTap(_ sender: Any) {
if let b = sender as? CircleTintedButton {
let n = b.layer.sublayers?.count
print("nunLayers: \(String(describing: n))")
}
}
You'll see that the sublayers count increases with each tap.
Add a gradient layer as a var / property of your custom button, and then only add it once:
#IBDesignable class CircleTintedButton: UIButton {
#IBInspectable var cornerRadius : CGFloat = 1.0
#IBInspectable var shadowOffsetWidth: CGFloat = 0.0
#IBInspectable var shadowOffsetHeight: CGFloat = 2.0
#IBInspectable var shadowColor : UIColor = UIColor.gray
#IBInspectable var shadowOpacity: CGFloat = 0.3
// add this var / property
private var gradLayer: CAGradientLayer?
#IBInspectable var startColor: UIColor = .blue {
didSet {
setNeedsLayout()
}
}
#IBInspectable var endColor: UIColor = .green {
didSet {
setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = cornerRadius
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
layer.shadowPath = shadowPath.cgPath
layer.shadowOpacity = Float(shadowOpacity)
// only create / add the gradient layer once
if gradLayer == nil {
let gradientLayer = CAGradientLayer()
gradientLayer.locations = [0, 1]
gradientLayer.masksToBounds = true
layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)
self.gradLayer = gradientLayer
}
gradLayer?.colors = [startColor.cgColor, endColor.cgColor]
gradLayer?.startPoint = CGPoint(x: 0.0, y: 0.0)
gradLayer?.endPoint = CGPoint(x: 1.0, y: 0.6)
gradLayer?.frame = bounds
gradLayer?.cornerRadius = cornerRadius
// original code
// let gradientLayer = CAGradientLayer()
// gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
// gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
// gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.6)
//
// gradientLayer.locations = [0, 1]
// gradientLayer.frame = bounds
// gradientLayer.cornerRadius = cornerRadius
// gradientLayer.masksToBounds = true
//
// layer.insertSublayer(gradientLayer, below: self.titleLabel?.layer)
}
override var isHighlighted: Bool {
get {
return super.isHighlighted
}
set {
let xScale : CGFloat = newValue ? 1.025 : 1.0
let yScale : CGFloat = newValue ? 1.1 : 1.0
UIView.animate(withDuration: 0.1) {
let transformation = CGAffineTransform(scaleX: xScale, y: yScale)
self.transform = transformation
}
super.isHighlighted = newValue
}
}
}

Related

Vertical Progress Bar with Gradient Layer

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
}
}

How to create a rotating rainbow color circle in iOS

From stackoverflow i got a code for drawing rainbow color circle.But as part of requirement ,I need that circle to be rotated continously ,like a rotating progress loader.Below is the code used for creating Rainbow color circle.
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
// //Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
Please anyone help me in rotating this circle.
Please find below the circle generated with above code:
What you got looks completely overcomplicated in the first place. Take a look at the following example:
class ViewController: UIViewController {
class RainbowView: UIView {
var segmentCount: Int = 10 {
didSet {
refresh()
}
}
var lineWidth: CGFloat = 10 {
didSet {
refresh()
}
}
override var frame: CGRect {
didSet {
refresh()
}
}
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
private var currentGradientLayer: CAGradientLayer?
private func refresh() {
currentGradientLayer?.removeFromSuperlayer()
guard segmentCount > 0 else { return }
currentGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.type = .conic
let colors: [UIColor] = {
var colors: [UIColor] = [UIColor]()
for i in 0..<segmentCount {
colors.append(UIColor(hue: CGFloat(i)/CGFloat(segmentCount), saturation: 1, brightness: 1, alpha: 1))
}
colors.append(UIColor(hue: 0.0, saturation: 1, brightness: 1, alpha: 1)) // Append start color at the end as well to complete the circle
return colors;
}()
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
gradientLayer.mask = {
let shapeLayer = CAShapeLayer()
shapeLayer.frame = bounds
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = UIBezierPath(ovalIn: bounds.inset(by: UIEdgeInsets(top: lineWidth*0.5, left: lineWidth*0.5, bottom: lineWidth*0.5, right: lineWidth*0.5))).cgPath
return shapeLayer
}()
return gradientLayer
}()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview({
let view = RainbowView(frame: CGRect(x: 50.0, y: 100.0, width: 100.0, height: 100.0))
var angle: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { _ in
angle += 0.01
view.transform = CGAffineTransform(rotationAngle: angle)
})
return view
}())
}
}
So a view is generated that uses a conical gradient with mask to draw the circle you are describing. Then a transform is applied to the view to rotate it. And a Timer is scheduled to rotate the circle.
Note that this code will leak because timer is nowhere invalidated. It needs to be removed when view disappears or similar.
The easiest way would be to attach an animation that repeats forever:
let animation = CABasicAnimation(keyPath: "transform.rotation") // Create rotation animation
animation.repeatCount = .greatestFiniteMagnitude // Repeat animation for as long as we can
animation.fromValue = 0 // Rotate from 0
animation.toValue = 2 * Float.pi // to 360 deg
animation.duration = 1 // During 1 second
self.layer.add(animation, forKey: "animation") // Adding the animation to the view
self - is RainbowCircle, assuming that you add this code to one of the methods inside it.
For this we can have Image something like this
syncImage.image = UIImage(named:"spinning")
Create a below extension to Start/Stop Rotating
extension UIView {
// To animate
func startRotating(duration: Double = 1) {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) == nil {
let animate = CABasicAnimation(keyPath: "transform.rotation")
animate.duration = duration
animate.repeatCount = Float.infinity
animate.fromValue = 0.0
animate.toValue = Float(M_PI * 2.0)
self.layer.addAnimation(animate, forKey: kAnimationKey)
}
}
func stopRotating() {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) != nil {
self.layer.removeAnimationForKey(kAnimationKey)
}
}
}
Usage
func startSpinning() {
syncImage.startRotating()
}
func stopSpinning() {
syncImage.stopRotating()
}
func handleSyncTap(sender: UITapGestureRecognizer? = nil) {
startSpinning()
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.stopSpinning()
})
}

UIView horizontal bar animation in swift

I am working on this animation where a number will be received every second and progress bar has to fill or go down based on the double value.
I have created the views and have added all the views in the UIStackView. Also made the outlet collection for all the views. (sorting them by the tag and making them round rect).
I can loop the views and change their background color but trying to see if there is a better way to do it. Any suggestions?
Thanks
So how you are doing it is fine. Here would be two different ways. The first with Core Graphics. You may want to update methods and even make the color gradient in the sublayer.
1st Way
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
var fillReversed = false
override init(frame: CGRect) {
super.init(frame: frame)
setUp(animated: false)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp(animated: false)
backgroundColor = UIColor.green
}
func updateProgress(progress:Double, animated:Bool) {
currentProgress = progress
setUp(animated: animated)
}
private func setUp(animated:Bool){
// internal space
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
if animated == true{
let trans = CATransition()
trans.type = kCATransitionFade
trans.duration = 0.5
self.layer.add(trans, forKey: nil)
}
layer.sublayers?.removeAll()
for i in 0...Int(indicators - 1.0){
let indicatorLayer = CALayer()
indicatorLayer.frame = CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height)
//haha i don't understand my logic below but it works hahaha
// i know it has to go backwards
if fillReversed{
if CGFloat(1 - currentProgress) * self.bounds.height < indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}
}else{
if CGFloat(currentProgress) * self.bounds.height > indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}
}
indicatorLayer.cornerRadius = indicatorLayer.frame.height * radiusFactor
indicatorLayer.masksToBounds = true
self.layer.addSublayer(indicatorLayer)
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
setUp(animated: false)
}
}
The second way is using CAShapeLayer and the benefit is that the progress will get a natural animation.
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
private var progressLayer : CALayer?
private var shapeHoles : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
transparentDotsAndProgress()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
transparentDotsAndProgress()
}
func updateProgress(progress:Double) {
if progress <= 1 && progress >= 0{
currentProgress = progress
transparentDotsAndProgress()
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
transparentDotsAndProgress()
}
func transparentDotsAndProgress(){
self.layer.masksToBounds = true
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height), cornerRadius: 0)
path.usesEvenOddFillRule = true
for i in 0...Int(indicators - 1.0){
let circlePath = UIBezierPath(roundedRect: CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height), cornerRadius: height * radiusFactor)
path.append(circlePath)
}
if progressLayer == nil{
progressLayer = CALayer()
progressLayer?.backgroundColor = filledIndicatorColor.cgColor
self.layer.addSublayer(progressLayer!)
}
progressLayer?.frame = CGRect(x: 0, y: -self.bounds.height - padding + CGFloat(currentProgress) * self.bounds.height, width: bounds.width, height: bounds.height)
self.shapeHoles?.removeFromSuperlayer()
shapeHoles = CAShapeLayer()
shapeHoles?.path = path.cgPath
shapeHoles?.fillRule = kCAFillRuleEvenOdd
shapeHoles?.fillColor = UIColor.white.cgColor
shapeHoles?.strokeColor = UIColor.clear.cgColor
self.layer.backgroundColor = indicatorColor.cgColor
self.layer.addSublayer(shapeHoles!)
}
}
Both of these ways should work but the advantage of the CAShapeLayer is you get a natural animation.
I'm a firm believer in learning through solving organic problems and slowly building my global knowledge on a subject. So I'm afraid I don't have any good tutorials for you.
Here is an example that will jump start you, though.
// For participating in Simulator's "slow animations"
#_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
import UIKit
#IBDesignable
class VerticalProgessView: UIControl {
#IBInspectable
var numberOfSegments: UInt = 0
#IBInspectable
var verticalSegmentGap: CGFloat = 4.0
#IBInspectable
var outerColor: UIColor = UIColor(red: 33, green: 133, blue: 109)
#IBInspectable
var unfilledColor: UIColor = UIColor(red: 61, green: 202, blue: 169)
#IBInspectable
var filledColor: UIColor = UIColor.white
private var _progress: Float = 0.25
#IBInspectable
open var progress: Float {
get {
return _progress
}
set {
self.setProgress(newValue, animated: false)
}
}
let progressLayer = CALayer()
let maskLayer = CAShapeLayer()
var skipLayoutSubviews = false
open func setProgress(_ progress: Float, animated: Bool) {
if progress < 0 {
_progress = 0
} else if progress > 1.0 {
_progress = 1
} else {
// Clamp the percentage to discreet values
let discreetPercentageDistance: Float = 1.0 / 28.0
let nearestProgress = discreetPercentageDistance * round(progress/discreetPercentageDistance)
_progress = nearestProgress
}
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
self?.skipLayoutSubviews = false
}
if !animated {
CATransaction.setDisableActions(true)
} else {
CATransaction.setAnimationDuration(0.25 * Double(UIAnimationDragCoefficient()))
}
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
skipLayoutSubviews = true
CATransaction.commit() // This triggers layoutSubviews
}
override func prepareForInterfaceBuilder() {
awakeFromNib()
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
self.layer.backgroundColor = unfilledColor.cgColor
// Initialize and add the progressLayer
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
progressLayer.backgroundColor = filledColor.cgColor
self.layer.addSublayer(progressLayer)
// Initialize and add the maskLayer (it has the holes)
maskLayer.frame = self.layer.bounds
maskLayer.fillColor = outerColor.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = maskPath(for: maskLayer.bounds)
self.layer.addSublayer(maskLayer)
// Layer hierarchy
// self.maskLayer
// self.progressLayer
// self.layer
}
override func layoutSubviews() {
super.layoutSubviews()
if skipLayoutSubviews {
// Crude but effective, not fool proof though
skipLayoutSubviews = false
return
}
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Doesn't work for 180° rotations
let duration = UIApplication.shared.statusBarOrientationAnimationDuration * Double(UIAnimationDragCoefficient())
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
CATransaction.setAnimationDuration(duration)
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
let size = self.bounds.size
let anchorPoint = CGPoint(x: 0.5, y: 1.0)
maskLayer.anchorPoint = anchorPoint
maskLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
maskLayer.position = CGPoint(x: size.width * anchorPoint.x, y: size.height * anchorPoint.y)
// Animate the segments
let pathChangeAnimation = CAKeyframeAnimation(keyPath: "path")
let finalPath = maskPath(for: maskLayer.bounds)
pathChangeAnimation.values = [maskLayer.path!, finalPath]
pathChangeAnimation.keyTimes = [0, 1]
pathChangeAnimation.timingFunction = timingFunction
pathChangeAnimation.duration = duration
pathChangeAnimation.isRemovedOnCompletion = true
maskLayer.add(pathChangeAnimation, forKey: "pathAnimation")
CATransaction.setCompletionBlock {
// CAAnimation's don't actually change the value
self.maskLayer.path = finalPath
}
CATransaction.commit()
}
// Provides a path that will mask out all the holes to show self.layer and the progressLayer behind
private func maskPath(for rect: CGRect) -> CGPath {
let horizontalSegmentGap: CGFloat = 5.0
let segmentWidth = rect.width - horizontalSegmentGap * 2
let segmentHeight = rect.height/CGFloat(numberOfSegments) - verticalSegmentGap + verticalSegmentGap/CGFloat(numberOfSegments)
let segmentSize = CGSize(width: segmentWidth, height: segmentHeight)
let segmentRect = CGRect(x: horizontalSegmentGap, y: 0, width: segmentSize.width, height: segmentSize.height)
let path = CGMutablePath()
for i in 0..<numberOfSegments {
// Literally, just move it down by the y value here
// this simplifies the math of calculating the starting points and what not
let transform = CGAffineTransform.identity.translatedBy(x: 0, y: (segmentSize.height + verticalSegmentGap) * CGFloat(i))
let segmentPath = UIBezierPath(roundedRect: segmentRect, cornerRadius: segmentSize.height / 2)
segmentPath.apply(transform)
path.addPath(segmentPath.cgPath)
}
// Without the outerPath, we'll end up with a bunch of squircles instead of a bunch of holes
let outerPath = CGPath(rect: rect, transform: nil)
path.addPath(outerPath)
return path
}
/// Provides the current and correct bounds and position for the progressLayer
private func progressLayerProperties() -> (bounds: CGRect, position: CGPoint) {
let frame = self.bounds
let height = frame.height * CGFloat(progress)
let y = frame.height * CGFloat(1 - progress)
let width = frame.width
let anchorPoint = maskLayer.anchorPoint
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let position = CGPoint(x: 0 + width * anchorPoint.x, y: y + height * anchorPoint.x)
return (bounds: bounds, position: position)
}
// TODO: Implement functions to further mimic UIProgressView
}
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
}
}
Using in a storyboard
Enjoy the magic

Set Background Gradient on Button in Swift

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
}
}

How could I set gradient fixed background on UICollectionView?

I used following code.
CAGradientLayer* collectionRadient = [CAGradientLayer layer];
collectionRadient.bounds = self.collectionView.bounds;
collectionRadient.anchorPoint = CGPointZero;
collectionRadient.colors = [NSArray arrayWithObjects:(id)[startColor CGColor],(id)[endColor CGColor], nil];
[self.collectionView.layer insertSublayer:collectionRadient atIndex:0];
But it drawn on cells included images. so cell was not shown.
I want to draw gradient background of UICollectionView under Cells and fixed it when view scrolled.
Let me know Please.
Try this... You have to assign a view to use background view.
CAGradientLayer* collectionGradient = [CAGradientLayer layer];
collectionGradient.bounds = self.view.bounds;
collectionGradient.anchorPoint = CGPointZero;
collectionGradient.colors = [NSArray arrayWithObjects:(id)[[UIColor redColor] CGColor],(id)[[UIColor greenColor] CGColor], nil];
UIView *vv = [[UIView alloc] init];
vview.backgroundView = vv;
[vview.backgroundView.layer insertSublayer:collectionGradient atIndex:0];
In Swift 3.0
I like to start with a custom class for gradients
import UIKit
#IBDesignable
class GradientView: UIView {
//set your start color
#IBInspectable var startColor: UIColor = UIColor.black { didSet { updateColors() }}
//set your end color
#IBInspectable var endColor: UIColor = UIColor.white { didSet { updateColors() }}
//you can change anchors and directions
#IBInspectable var startLocation: Double = 0.05 { didSet { updateLocations() }}
#IBInspectable var endLocation: Double = 0.95 { didSet { updateLocations() }}
#IBInspectable var horizontalMode: Bool = false { didSet { updatePoints() }}
#IBInspectable var diagonalMode: Bool = false { didSet { updatePoints() }}
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
func updatePoints() {
if horizontalMode {
gradientLayer.startPoint = diagonalMode ? CGPoint(x: 1, y: 0) : CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = diagonalMode ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0.5)
} else {
gradientLayer.startPoint = diagonalMode ? CGPoint(x: 0, y: 0) : CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = diagonalMode ? CGPoint(x: 1, y: 1) : CGPoint(x: 0.5, y: 1)
}
}
func updateLocations() {
gradientLayer.locations = [startLocation as NSNumber, endLocation as NSNumber]
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
override func layoutSubviews() {
super.layoutSubviews()
updatePoints()
updateLocations()
updateColors()
}
}
With the custom class somewhere in the project and added to the target you just need to do as mentioned earlier and add it as a background view rather than background color or separate normal view sent to the back
Implement like so:
let gradient = GradientView()
gradient.frame = self.view.bounds
//add to your collectionView
collectionView?.addSubview(gradient)
collectionView?.sendSubview(toBack: gradient)
self.collectionView?.backgroundView = gradient
Directions
Create a view
Assign that view to the backgroundView of you collectionView
Create a gradient
Add gradient layer to collectionView backgroundView
Code Swift 4.2
private func addBackgroundGradient() {
let collectionViewBackgroundView = UIView()
let gradientLayer = CAGradientLayer()
gradientLayer.frame.size = view.frame.size
// Start and end for left to right gradient
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
collectionView.backgroundView = collectionViewBackgroundView
collectionView.backgroundView?.layer.addSublayer(gradientLayer)
}
If you want a top to bottom gradient all you have to do is remove endPoint and startPoint. You can read more about gradients here.
Note that startPoint and endPoint are defined in the coordinate plane from (0.0, 0.0) to (1.0, 1.0)

Resources