Reputation: 1059
I have a scaling/pulsing animation that is working on cgPaths in a for loop as below. This code works but only when you append the animatedLayers
to the circleLayer
path when the circleLayer
has already been added to the subLayer
and this creates a static circle (glitch-like) before the animation (DispatchQueue
) starts...
...
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
...
Is it possible to add a CAShapeLayer
with arguments to a subLayer? If not, any recommended alternative?
import UIKit
import Foundation
@IBDesignable
class AnimatedCircleView: UIView {
// MARK: - Initializers
var animatedLayers = [CAShapeLayer]()
// MARK: - Methods
override func draw(_ rect: CGRect) {
// Animated circle
for _ in 0...3 {
let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let animatedLayer = CAShapeLayer()
animatedLayer.path = animatedPath.cgPath
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.gray.cgColor
animatedLayer.lineCap = CAShapeLayerLineCap.round
animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
// Dispatch animation for circle _ 0...3
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.animateCircle(index: 3)
}
}
}
}
}
func animateCircle(index: Int) {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.8
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
scaleAnimation.repeatCount = Float.infinity
animatedLayers[index].add(scaleAnimation, forKey: "scale")
let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
opacityAnimation.duration = 1.8
opacityAnimation.fromValue = 0.7
opacityAnimation.toValue = 0
opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
opacityAnimation.repeatCount = Float.infinity
animatedLayers[index].add(opacityAnimation, forKey: "opacity")
}
}
Upvotes: 2
Views: 344
Reputation: 437917
The key issue is that your animations start, with staggered delays, between in 0.1 and 1.0 seconds. Until that last animation starts, that layer is just sitting there, full sized and at 100% opacity.
Since you are animating the transform scale from 0 to 1, I’d suggest setting the starting transform to 0 (or changing the opacity
to 0). Then you won’t see them sitting there until their respective animations start.
A few other observations:
The draw(_:)
is not the right place to add layers, start animations, etc. This method may be called multiple times and should represent the view at a given point in time. I’d retire draw(_:)
is it’s not the right place to start this and you don’t need this method at all.
You start your first animation after 0.1 seconds. Why not start it immediately?
You should handle frame adjustments by deferring the setting of the path
and position
properties until layoutSubviews
.
Thus:
@IBDesignable
class AnimatedCircleView: UIView {
private var animatedLayers = [CAShapeLayer]()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(arcCenter: .zero, radius: bounds.width / 2.3, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
for animatedLayer in animatedLayers {
animatedLayer.path = path.cgPath
animatedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
}
}
}
// MARK: - Methods
private extension AnimatedCircleView {
func configure() {
for _ in 0...3 {
let animatedLayer = CAShapeLayer()
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.gray.cgColor
animatedLayer.lineCap = .round
animatedLayer.transform = CATransform3DMakeScale(0, 0, 1)
layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 3)
}
}
}
}
func animateCircle(index: Int) {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.8
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
scaleAnimation.repeatCount = .greatestFiniteMagnitude
animatedLayers[index].add(scaleAnimation, forKey: "scale")
let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
opacityAnimation.duration = 1.8
opacityAnimation.fromValue = 0.7
opacityAnimation.toValue = 0
opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
opacityAnimation.repeatCount = .greatestFiniteMagnitude
animatedLayers[index].add(opacityAnimation, forKey: "opacity")
}
}
Upvotes: 1
Reputation: 11083
Have you tried starting with the fillcolor as clear then turning it to grey at the start of the animation?
// Animated circle
for _ in 0...3 {
let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let animatedLayer = CAShapeLayer()
animatedLayer.path = animatedPath.cgPath
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.clear.cgColor
animatedLayer.lineCap = CAShapeLayerLineCap.round
animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
// Dispatch animation for circle _ 0...3
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.animateCircle(index: 3)
}
}
}
}
}
Upvotes: 0