I'm attempting to add a circular loading animation inside of a UICollectionViewCell. I've added the exact same code to a normal UIViewController and it worked successfully but I don't get any results on a UICollectionViewCell.
Ideally I'd like to have the animation start when the cell becomes visible.
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
func setupBezierPaths() {
let center = self.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
// Create a track layer
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.lineCap = CAShapeLayerLineCap.round
trackLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(trackLayer)
// Create a layer that is animated
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 10
shapeLayer.strokeEnd = 0
shapeLayer.lineCap = CAShapeLayerLineCap.round
shapeLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(shapeLayer)
}
For the UICollectionViewCell, I'm not sure where to put the actual animation code. I've tried it inside the init but the track layer doesn't even show up. Here is the code for the animation:
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 1
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "simpleAnimation")
Related
I am using an arc for a progress bar.
There is a track layer that the progress indicator moves along and I also have a circle that moves with the progress bar for added emphasis. Both the progress bar and the circle move correctly along the path but when the progress is complete, instead of stopping at the end of the path, the circle jumps back to the beginning of the progress path.
screen recording of behavior
The code:
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let dotLayer: CALayer = CALayer()
let seconds = 10.0
// MARK: - Properties
private var progressBarView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
title = "Making Progress"
view.backgroundColor = .systemTeal
// determine the value of Center
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: 2.7, endAngle: 0.45, clockwise: true) // these values give us a complete circle
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
view.layer.addSublayer(shapeLayer)
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap )))
}
#objc private func handleTap() {
print("Attempting to animate stroke")
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = seconds
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "anyKeyWillDo")
// circle image
let circleNobImage = UIImage(named: "whiteCircle.png")!
dotLayer.contents = circleNobImage.cgImage
dotLayer.bounds = CGRect(x: 0.0, y: 0.0, width: circleNobImage.size.width, height: circleNobImage.size.height)
view.layer.addSublayer(dotLayer)
dotLayer.position = CGPoint(x: 105, y: 460)
dotLayer.opacity = 1
dotAnimation()
}
func dotAnimation() {
let dotAnimation = CAKeyframeAnimation(keyPath: "position")
dotAnimation.path = trackLayer.path
dotAnimation.calculationMode = .paced
let dotAnimationGroup = CAAnimationGroup()
dotAnimationGroup.duration = seconds
dotAnimation.autoreverses = false
dotAnimationGroup.animations = [dotAnimation]
dotLayer.add(dotAnimationGroup, forKey: nil)
}
}
I need to get the circle to stop at the end of the arc with the progress bar. How is this done?
Also set dotAnimationGroup.fillMode and dotAnimationGroup.isRemovedOnCompletion
dotAnimationGroup.fillMode = .forwards
dotAnimationGroup.isRemovedOnCompletion = false
Inside the circle progress bar there is a constantly changing number (eg 250, 300, 1000). Whenever I click, the number will decrease and circle progress bar will move. I did it with the time counter. But I want to do it with my control. So when I click the button it will move, if I don't click it won't move.
My code :
import UIKit
class ViewController: UIViewController {
let shapeLAyer = CAShapeLayer()
let progressLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 2*CGFloat.pi, clockwise: true)
progressLayer.path = circularPath.cgPath
// ui edits
progressLayer.strokeColor = UIColor.black.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
//progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 20.0
view.layer.addSublayer(progressLayer)
shapeLAyer.path = circularPath.cgPath
shapeLAyer.fillColor = UIColor.clear.cgColor
shapeLAyer.strokeColor = UIColor.red.cgColor
shapeLAyer.lineWidth = 10
shapeLAyer.lineCap = .round
shapeLAyer.strokeEnd = 0
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handle)))
view.layer.addSublayer(shapeLAyer)
}
#objc func handle(){
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 3
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
}
[![enter image description here][1]][1]
You can achieve this type of progress by setting proper from value, to value and with the mode of CABasicAnimation
Here, I take two var one for total number (for total progress) and one for current progress number.
From these two var, I converted this value to between 0-1.
First, in your code UIBezierPath angle is wrong.
Replace this line by
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 3*CGFloat.pi / 2, clockwise: true)
class ViewController: UIViewController {
// Other code
private let totalNumber: CGFloat = 300.0
private var currentProgressNumber: CGFloat = 0
private let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
// viewDidLoad code
func playAnimation(){
if currentProgressNumber > totalNumber || currentProgressNumber < 0 {
return
}
basicAnimation.fromValue = basicAnimation.toValue
basicAnimation.toValue = currentProgressNumber/totalNumber
basicAnimation.isRemovedOnCompletion = false
basicAnimation.duration = 3
basicAnimation.fillMode = CAMediaTimingFillMode.both
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
#IBAction func onIncrement(_ sender: UIButton) {
currentProgressNumber += 15 //<-- Change your increment interval here
playAnimation()
}
#IBAction func onDecrement(_ sender: UIButton) {
currentProgressNumber -= 15 //<-- Change your increment interval here
playAnimation()
}
}
Note: Replace your animation function with playAnimation() and also change the increment-decrement value as per your requirement for testing I used 15.
that's the code I use to create shape it have to be in viewDidLayoutSubviews because I'm centring it to view bounds which aren't loaded in viewDidLoad ,but for some reason it creates the shape twice... Can someone help me get rid of the second shape?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let center = circleView.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
// create my track layer
let trackLayer = CAShapeLayer()
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.white.cgColor
trackLayer.lineWidth = 5.5
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = CAShapeLayerLineCap.round
view.layer.addSublayer(trackLayer)
// let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
shapeLayer.lineWidth = 5.5
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = CAShapeLayerLineCap.round
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
}
I was able to solve it after putting let trackLayer = CAShapeLayer() outside of viewDidLayoutSubviews
i have a custom view that is a 85% of a circle ( Just Stroke ) with Gradient color . here is my code :
class ProfileCircle:UIView
{
override func draw(_ rect: CGRect)
{
let desiredLineWidth:CGFloat = 4 // your desired value
let arcCenter = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let radius = self.bounds.midX
let circlePath = UIBezierPath(
arcCenter: arcCenter,
radius: radius,
startAngle: CGFloat(0),
endAngle:CGFloat((.pi * 2)*0.85),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = desiredLineWidth
let gradient = CAGradientLayer()
gradient.frame = circlePath.bounds
gradient.colors = [UIColor.colorPrimary.cgColor, UIColor.colorSecondary.cgColor]
//layer.addSublayer(shapeLayer)
gradient.mask = shapeLayer
layer.addSublayer(gradient)
}
}
but the edges are clipped
i have tried a lot but i cant fix it .
one more thing is that when i change the radius manually the view wont be centered
Just two changes:
let radius = self.bounds.midX - desiredLineWidth
and
gradient.frame = self.bounds
I'm new to Swift and I don't have too much information about it.I would like to create animated clock with filling 'CAShapeLayer' path in time interval than tried to make it with 'CABasicAnimation' in 60 seconds. The shape fills in 49 sec and animation finish fit in 60 seconds, so it's not working as desired. Than I changed animation.byValue = 0.1 but the result is same. Some one have an idea about this issue?
My code is :
var startAngle = -CGFloat.pi / 2
var endAngle = CGFloat.pi * 2
var shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
animate(layer: shapeLayer) //Animation func
view.layer.addSublayer(shapeLayer)
}
private func animate(layer: CAShapeLayer) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 60.0 //I think is counting by seconds?
animation.repeatCount = .infinity
animation.fillMode = kCAFillModeForwards
layer.add(animation, forKey: "strokeEndAnimation")
}
The problem is with the way you made the BezierPath.
Try this instead:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.view.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)
The arcCenter must be at .zero and set the shape's center as the center of the view. Your circle will start animating from the right most point though, therefore add a CATransform to rotate the shape 90 degrees counterclockwise.