How to animate custom Progress bar properly (swift)? - ios

I made a custom progressbar for my app (following an article on medium), it works as intended but i have one problem, when i change the progress value then it jumps to fast! (dont get confused by the percent values below the bar, they are off, i know that)
i use setNeedsDisplay() to redraw my view.
I want the bar to animate smoothly, so in my case a bit slower.
this is the draw function of the bar:
override func draw(_ rect: CGRect) {
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.25).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
}
Here is the whole Class i used:
https://bitbucket.org/mariwi/custom-animated-progress-bars-with-uibezierpaths/src/master/ProgressBars/Bars/GradientHorizontalProgressBar.swift
Anyone with an idea?
EDIT 1:
Similar questions helped, but the result is not working properly.
I aded this function to set the progress of the bar:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .forwards
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = 0.5
progressLayer.bounds = newBounds
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
}
And now the bar behaves like this:

After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.
First i set the position back to (0,0) so that the view started at the beginning (where it should).
The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.
After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:
Here is the final code, i used to accomplish this:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let duration = 0.5
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .both
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = duration
progressLayer.bounds = newBounds
progressLayer.position = CGPoint(x: 0, y: 0)
progressLayer.anchorPoint = CGPoint(x: 0, y: 0)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
let oldGradEnd = gradientLayer.endPoint
let newGradEnd = CGPoint(x: progress, y: 0.5)
let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
gradientEndAnimation.fromValue = oldGradEnd
gradientEndAnimation.toValue = newGradEnd
gradientEndAnimation.fillMode = .both
gradientEndAnimation.isRemovedOnCompletion = false
gradientEndAnimation.duration = duration
gradientLayer.endPoint = newGradEnd
gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")
}

I'm going to suggest a somewhat different approach.
First, instead of adding a sublayer as the gradient layer, we'll make the custom view's layer itself a gradient layer:
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
// then, in init
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
We'll set the gradient animation to the full size of the view... that will give it a consistent width and speed.
Next, we'll add a subview as a mask, instead of a layer-mask. That will allow us to animate its width independently.
class GradProgressView: UIView {
#IBInspectable var color: UIColor = .gray {
didSet { setNeedsDisplay() }
}
#IBInspectable var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
// this view will mask the percentage width
private let myMaskView = UIView()
// so we can calculate the new-progress-animation duration
private var curProgress: CGFloat = 0.0
public var progress: CGFloat = 0 {
didSet {
// calculate the change in progress
let changePercent = abs(curProgress - progress)
// if the change is 100% (i.e. from 0.0 to 1.0),
// we want the animation to take 1-second
// so, make the animation duration equal to
// 1-second * changePercent
let dur = changePercent * 1.0
// save the new progress
curProgress = progress
// calculate the new width of the mask view
var r = bounds
r.size.width *= progress
// animate the size of the mask-view
UIView.animate(withDuration: TimeInterval(dur), animations: {
self.myMaskView.frame = r
})
}
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.locations = [0.25, 0.5, 0.75]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.3, -0.15, 0]
animation.toValue = [1, 1.15, 1.3]
animation.duration = 1.5
animation.isRemovedOnCompletion = false
animation.repeatCount = Float.infinity
gradientLayer.add(animation, forKey: nil)
myMaskView.backgroundColor = .white
mask = myMaskView
}
override func layoutSubviews() {
super.layoutSubviews()
// if the mask view frame has not been set at all yet
if myMaskView.frame.height == 0 {
var r = bounds
r.size.width = 0.0
myMaskView.frame = r
}
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
layer.cornerRadius = bounds.height * 0.25
}
}
Here's a sample controller class - each tap will cycle through a list of sample progress percentages:
class ExampleViewController: UIViewController {
let progView = GradProgressView()
let infoLabel = UILabel()
var idx: Int = 0
let testVals: [CGFloat] = [
0.75, 0.3, 0.95, 0.25, 0.5, 1.0,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
[infoLabel, progView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
infoLabel.textColor = .white
infoLabel.textAlignment = .center
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
progView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
progView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
progView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
progView.heightAnchor.constraint(equalToConstant: 40.0),
infoLabel.topAnchor.constraint(equalTo: progView.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
progView.color = #colorLiteral(red: 0.9932278991, green: 0.5762576461, blue: 0.03188031539, alpha: 1)
progView.gradientColor = #colorLiteral(red: 1, green: 0.8578521609, blue: 0.3033572137, alpha: 1)
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didTap(nil)
}
#objc func didTap(_ g: UITapGestureRecognizer?) -> Void {
let n = idx % testVals.count
progView.progress = testVals[n]
idx += 1
infoLabel.text = "Auslastung \(Int(testVals[n] * 100))%"
}
}

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

Create a layer with increasing height - swift

I want to make a custom slider that has increasing height i.e it's height starts from 4.0 and goes to 6.0.
I have written code for creating a layer but I cannot find a way to increase its height in this manner. Here is my code :
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
ctx.addPath(path.cgPath)
ctx.setFillColor(UIColor.red.cgColor)
ctx.fillPath()
let lowerValuePosition = slider.positionForValue(slider.lowerValue)
let upperValuePosition = slider.positionForValue(slider.upperValue)
let rect = CGRect(x: 0, y: 0,
width: (bounds.width - 36),
height: bounds.height)
ctx.fill(rect)
Because the Minimum and Maximum (left-side & right-side) "Track" images stretch, you may not be able to get what you want with a default UISlider.
Not too tough to get around it though.
Basically:
create a custom view with your "rounded wedge" shape
overlay a UISlider on that custom view
"fill" the percentage of the shape when the slider value changes
Here's the idea, before overlaying them:
When we want to overlay the slider on the wedge, set the slider Min/Max track images to clear and it looks like this:
We can use a little trick to handle "filling" the shape by percentage:
use a gradient background layer
mask it with the shape
set the gradient colors to red, red, gray, gray
set the color locations to [0.0, pct, pct, 1.0]
That way we get a clean edge, instead of a gradient fade.
Here's a complete example -- no #IBOutlet or #IBAction connections, so just set a view controller's custom class to WedgeSliderViewController:
class RoundedWedgeSliderView: UIView {
var leftRadius: CGFloat = 4.0
var rightRadius: CGFloat = 6.0
// mask shape
private var cMask = CAShapeLayer()
var pct: Float = 0.0 {
didSet {
let p = pct as NSNumber
// disable layer built-in animation so the update won't "lag"
CATransaction.begin()
CATransaction.setDisableActions(true)
// update gradient locations
gradientLayer.locations = [
0.0, p, p, 1.0
]
CATransaction.commit()
}
}
// allows self.layer to be a CAGradientLayer
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// gradient colors will be
// red, red, gray, gray
let colors = [
UIColor.red.cgColor,
UIColor.red.cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
]
gradientLayer.colors = colors
// initial gradient color locations
gradientLayer.locations = [
0.0, 0.0, 0.0, 1.0
]
// horizontal gradient
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
// define the "Rounded Wedge" shape
let leftCenter = CGPoint(x: r.minX + leftRadius, y: r.midY)
let rightCenter = CGPoint(x: r.maxX - rightRadius, y: r.midY)
let bez = UIBezierPath()
bez.addArc(withCenter: leftCenter, radius: leftRadius, startAngle: .pi * 0.5, endAngle: .pi * 1.5, clockwise: true)
bez.addArc(withCenter: rightCenter, radius: rightRadius, startAngle: .pi * 1.5, endAngle: .pi * 0.5, clockwise: true)
bez.close()
// set the mask layer's path
cMask.path = bez.cgPath
// mask self's layer
layer.mask = cMask
}
}
class WedgeSliderViewController: UIViewController {
let mySliderView = RoundedWedgeSliderView()
let theSlider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mySliderView)
view.addSubview(theSlider)
mySliderView.translatesAutoresizingMaskIntoConstraints = false
theSlider.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain slider 100-pts from top, 40-pts on each side
theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// constrain mySliderView width to the slider width minus 16-pts
// (so we have 8-pt "padding" on each side for the thumb to cover)
mySliderView.widthAnchor.constraint(equalTo: theSlider.widthAnchor, constant: -16.0),
// constrain mySliderView to same height as the slider, centered X & Y
mySliderView.heightAnchor.constraint(equalTo: theSlider.heightAnchor),
mySliderView.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
mySliderView.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
])
// set left- and right-side "track" images to empty images
theSlider.setMinimumTrackImage(UIImage(), for: .normal)
theSlider.setMaximumTrackImage(UIImage(), for: .normal)
// add target for the slider
theSlider.addTarget(self, action: #selector(self.sliderValueChanged(_:)), for: .valueChanged)
// set intitial values
theSlider.value = 0.0
mySliderView.pct = 0.0
// end-radii of mySliderView defaults to 4.0 and 6.0
// un-comment next line to see the difference
//mySliderView.rightRadius = 10.0
}
#objc func sliderValueChanged(_ sender: Any) {
if let s = sender as? UISlider {
// update mySliderView when the slider changes
mySliderView.pct = s.value
}
}
}
You have to override the trackRect method of your superclass UISlider in the subclass like below and return the bounds and size that you require:
/**
UISlider Subclass
*/
class CustomSlider: UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: #oigin#, size: CGSize(width: #width#, height: #height#))
}
}

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

iOS animate drawing of multiple circles, only one displays

Based on the answer to this question: Animate drawing of a circle
I now want to display two of these circles simultaneously on the same screen but in two different views. If I just want to animate one circle there are no problems. However if I try to add a second, only the second one is visible.
This is the Circle class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
viewLayer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
And here is how I call it from my ViewController:
var rythmTimer: Circle?
var adrenalineTimer: Circle?
override func viewDidLoad() {
// Create two timers as circles
self.rythmTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view1.layer)
if let rt = rythmTimer {
view1.addSubview(rt)
rt.center = CGPoint(x: self.view1.bounds.midX, y: self.view1.bounds.midY);
}
self.adrenalineTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view2.layer)
if let at = adrenalineTimer {
view2.addSubview(at)
at.center = CGPoint(x: self.view2.bounds.midX, y: self.view2.bounds.midY)
}
}
If I remove the code for the adrenalineTimer I can see the circle drawn by the rythmTimer. If I keep it the rythmTimer will be displayed in view2 instead of view1 and will have the duration/color of the rythmTimer
It seems you set the circle path with wrong origin points. In addition, you do it before you place the circle on the view.
Add this code to the animate function instead:
func animateCircle(duration: TimeInterval) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.origin.x + self.frame.size.width / 2, y: self.frame.origin.y + self.frame.size.height / 2), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
...
The main problem is that you have declared circleLayer outside of the class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
The result is that you only ever have ONE circleLayer object (instance).
If you move it inside the class, then each instance of Circle will have its own instance of circleLayer:
import UIKit
class Circle: UIView {
var circleLayer: CAShapeLayer!
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
There are a number of other odd things you're doing, but that is why you only get one animated circle.
Edit: Here's a modified version of your code that will allow you to use auto-layout instead of fixed / hard-coded sizes and positions. You can run this directly in a Playground page:
import UIKit
import PlaygroundSupport
class Circle: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Setup the CAShapeLayer with colors and line width
circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// We haven't set the path yet, so don't draw initially
circleLayer.strokeEnd = 0.0
// Add the layer to the self's layer
self.layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
// this will update whenever the frame size changes (makes it easy to use with auto-layout)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
class MyViewController : UIViewController {
var rythmTimer: Circle?
var adrenalineTimer: Circle?
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var view1: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
var view2: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .orange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
view.addSubview(view1)
view.addSubview(view2)
NSLayoutConstraint.activate([
view1.widthAnchor.constraint(equalToConstant: 120.0),
view1.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
view2.widthAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 40.0),
])
rythmTimer = Circle(frame: CGRect.zero)
adrenalineTimer = Circle(frame: CGRect.zero)
if let rt = rythmTimer,
let at = adrenalineTimer {
view1.addSubview(rt)
view2.addSubview(at)
rt.translatesAutoresizingMaskIntoConstraints = false
at.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rt.widthAnchor.constraint(equalToConstant: 100.0),
rt.heightAnchor.constraint(equalToConstant: 100.0),
rt.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
rt.centerYAnchor.constraint(equalTo: view1.centerYAnchor),
at.widthAnchor.constraint(equalToConstant: 100.0),
at.heightAnchor.constraint(equalToConstant: 100.0),
at.centerXAnchor.constraint(equalTo: view2.centerXAnchor),
at.centerYAnchor.constraint(equalTo: view2.centerYAnchor),
])
}
}
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
if let rt = rythmTimer,
let at = adrenalineTimer {
rt.removeCircle()
at.removeCircle()
rt.animateCircle(duration: 2.0)
at.animateCircle(duration: 2.0)
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Swift - Color fill animation

I'm relatively new to ios animations and I believe there's something wrong with the approach I took to animate UIView.
I will start with a UI screenshot to picture my problem more precisely:
There is a tableView cell with two labels and colorful filled circle
Anytime I introduce new value to the cell, I'd like to animate this left-most bulb so it looks like it's getting filled with red color.
This is the implementation od BadgeView, which is basically the aforementioned leftmost filled circle
class BadgeView: UIView {
var coeff:CGFloat = 0.5
override func drawRect(rect: CGRect) {
let topRect:CGRect = CGRectMake(0, 0, rect.size.width, rect.size.height*(1.0 - self.coeff))
UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0).setFill()
UIRectFill(topRect)
let bottomRect:CGRect = CGRectMake(0, rect.size.height*(1-coeff), rect.size.width, rect.size.height*coeff)
UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0).setFill()
UIRectFill(bottomRect)
self.layer.cornerRadius = self.frame.height/2.0
self.layer.masksToBounds = true
}
}
This is the way I achieve uneven fill - I introduced coefficient which I modify in viewController.
Inside my cellForRowAtIndexPath method I try to animate this shape using custom button with callback
let btn:MGSwipeButton = MGSwipeButton(title: "", icon: img, backgroundColor: nil, insets: ins, callback: {
(sender: MGSwipeTableCell!) -> Bool in
print("Convenience callback for swipe buttons!")
UIView.animateWithDuration(4.0, animations:{ () -> Void in
cell.pageBadgeView.coeff = 1.0
let frame:CGRect = cell.pageBadgeView.frame
cell.pageBadgeView.drawRect(frame)
})
return true
})
But it does nothing but prints to console
: CGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
Although I'd love to know the right answer and approach, it would be great to know, for education purpose, why this code doesn't work.
Thanks in advance
The error part of the problem seems to be this part of the code:
cell.pageBadgeView.drawRect(frame)
From the Apple docs on UIView drawRect:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
So if you'd change your code to:
cell.pageBadgeView.setNeedsDisplay()
You'll get rid of the error and see the badgeView filled correctly. However this won't animate it, since drawRect isn't animatable by default.
The easiest workaround to your problem would be for BadgeView to have an internal view for the fill color. I'd refactor the BadgeView as so:
class BadgeView: UIView {
private let fillView = UIView(frame: .zero)
private var coeff:CGFloat = 0.5 {
didSet {
// Make sure the fillView frame is updated everytime the coeff changes
updateFillViewFrame()
}
}
// Only needed if view isn't created in xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// Only needed if view isn't created in xib or storyboard
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
override func awakeFromNib() {
setupView()
}
private func setupView() {
// Setup the layer
layer.cornerRadius = bounds.height/2.0
layer.masksToBounds = true
// Setup the unfilled backgroundColor
backgroundColor = UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0)
// Setup filledView backgroundColor and add it as a subview
fillView.backgroundColor = UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0)
addSubview(fillView)
// Update fillView frame in case coeff already has a value
updateFillViewFrame()
}
private func updateFillViewFrame() {
fillView.frame = CGRect(x: 0, y: bounds.height*(1-coeff), width: bounds.width, height: bounds.height*coeff)
}
// Setter function to set the coeff animated. If setting it not animated isn't necessary at all, consider removing this func and animate updateFillViewFrame() in coeff didSet
func setCoeff(coeff: CGFloat, animated: Bool) {
if animated {
UIView.animate(withDuration: 4.0, animations:{ () -> Void in
self.coeff = coeff
})
} else {
self.coeff = coeff
}
}
}
In your button callback you'll just have to do:
cell.pageBadgeView.setCoeff(1.0, animated: true)
try this playground code
import UIKit
import CoreGraphics
var str = "Hello, playground"
class BadgeView: UIView {
var coeff:CGFloat = 0.5
func drawCircleInView(){
// Set up the shape of the circle
let size:CGSize = self.bounds.size;
let layer = CALayer();
layer.frame = self.bounds;
layer.backgroundColor = UIColor.blue().cgColor
let initialRect:CGRect = CGRect.init(x: 0, y: size.height, width: size.width, height: 0)
let finalRect:CGRect = CGRect.init(x: 0, y: size.height/2, width: size.width, height: size.height/2)
let sublayer = CALayer()
sublayer.frame = initialRect
sublayer.backgroundColor = UIColor.orange().cgColor
sublayer.opacity = 0.5
let mask:CAShapeLayer = CAShapeLayer()
mask.frame = self.bounds
mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
mask.fillColor = UIColor.black().cgColor
mask.strokeColor = UIColor.yellow().cgColor
layer.addSublayer(sublayer)
layer.mask = mask
self.layer.addSublayer(layer)
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnim.toValue = NSValue.init(cgRect:finalRect)
let anim = CAAnimationGroup()
anim.animations = [boundsAnim]
anim.isRemovedOnCompletion = false
anim.duration = 3
anim.fillMode = kCAFillModeForwards
sublayer.add(anim, forKey: nil)
}
}
var badgeView:BadgeView = BadgeView(frame:CGRect.init(x: 50, y: 50, width: 50, height: 50))
var window:UIWindow = UIWindow(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
window.backgroundColor = UIColor.red()
badgeView.backgroundColor = UIColor.green()
window.becomeKey()
window.makeKeyAndVisible()
window.addSubview(badgeView)
badgeView.drawCircleInView()
More Modification to Above code , anchor point code was missing in above code
```
var str = "Hello, playground"
class BadgeView: UIView {
var coeff:CGFloat = 0.7
func drawCircleInView(){
// Set up the shape of the circle
let size:CGSize = self.bounds.size;
let layerBackGround = CALayer();
layerBackGround.frame = self.bounds;
layerBackGround.backgroundColor = UIColor.blue.cgColor
self.layer.addSublayer(layerBackGround)
let initialRect:CGRect = CGRect.init(x: 0, y: size.height , width: size.width, height: 0)
let finalRect:CGRect = CGRect.init(x: 0, y: 0, width: size.width, height: size.height)
let sublayer = CALayer()
//sublayer.bounds = initialRect
sublayer.frame = initialRect
sublayer.anchorPoint = CGPoint(x: 0.5, y: 1)
sublayer.backgroundColor = UIColor.orange.cgColor
sublayer.opacity = 1
let mask:CAShapeLayer = CAShapeLayer()
mask.frame = self.bounds
mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
mask.fillColor = UIColor.black.cgColor
mask.strokeColor = UIColor.yellow.cgColor
layerBackGround.addSublayer(sublayer)
layerBackGround.mask = mask
self.layer.addSublayer(layerBackGround)
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnim.toValue = NSValue.init(cgRect:finalRect)
let anim = CAAnimationGroup()
anim.animations = [boundsAnim]
anim.isRemovedOnCompletion = false
anim.duration = 1
anim.fillMode = kCAFillModeForwards
sublayer.add(anim, forKey: nil)
}

Resources