How to prevent CABasicAnimation from scale all content inside UIView? - ios

I've wrote this to apply heart beat animation to CAShapeLayer, after this worked fine, I need to implement it to be behind UIButton (btnTest) without scaling the UIButton or any other content.
#IBOutlet weak var btnTest: UIButton!
let btnLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
btnTest.layer.removeAllAnimations()
let ovalPath = UIBezierPath(arcCenter: CGPoint(x: btnTest.frame.midX, y: btnTest.frame.midY), radius: 100, startAngle: 0*(CGFloat.pi / 180), endAngle: 360*(CGFloat.pi / 180), clockwise: true)
btnLayer.path = ovalPath.cgPath
btnLayer.fillColor = UIColor.red.cgColor
btnLayer.contentsGravity = "center"
btnLayer.opacity = 0.3
self.view.layer.addSublayer(btnLayer)
let theAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
theAnimation.duration = 0.75
theAnimation.repeatCount = Float.infinity
theAnimation.autoreverses = true
theAnimation.fromValue = 1.0
theAnimation.toValue = 1.2
theAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
self.view.layer.add(theAnimation, forKey: nil)
}
the is this result gif
another solution was changing this line :
self.view.layer.add(theAnimation, forKey: nil)
to this :
btnLayer.add(theAnimation, forKey: nil)
the result of this was this :gif
Any ideas to solve this problem !

You want to animate your btnLayer. The reason it's animating from the wrong place is probably that the layer's anchorPoint is at 0,0, where it should be set to 0.5, 0.5.
Edit:
Actually, I think the issue is where you put your btnLayer. You should make it a sublayer of your button view's layer, and give it a frame that's the bounds of the button's layer:
btnTest.layer.addSublayer(btnLayer)
btnLayer.frame = btnTest.layer.bounds

You can do it like this, just translate your layer alongside with scale animation. To do so you need to create a CAAnimationGroup.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
btnTest.layer.removeAllAnimations()
let ovalPath = UIBezierPath(arcCenter: CGPoint(x: btnTest.frame.midX, y: btnTest.frame.midY), radius: 100, startAngle: 0*(CGFloat.pi / 180), endAngle: 360*(CGFloat.pi / 180), clockwise: true)
btnLayer.path = ovalPath.cgPath
btnLayer.fillColor = UIColor.red.cgColor
btnLayer.anchorPoint = CGPoint.init(x: 0.5, y: 0.5)
btnLayer.contentsGravity = "center"
btnLayer.opacity = 0.3
self.view.layer.addSublayer(btnLayer)
let theAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
theAnimation.fromValue = 1.0
theAnimation.toValue = 1.2
let theAnimationTranslationX = CABasicAnimation(keyPath: "transform.translation.x")
theAnimationTranslationX.fromValue = btnTest.bounds.minX
theAnimationTranslationX.toValue = btnTest.bounds.minX - 40
let theAnimationTranslationY = CABasicAnimation(keyPath: "transform.translation.y")
theAnimationTranslationY.fromValue = btnTest.bounds.minY
theAnimationTranslationY.toValue = btnTest.bounds.minY - 80
let animationGroup = CAAnimationGroup.init()
animationGroup.duration = 0.75
animationGroup.repeatCount = Float.infinity
animationGroup.autoreverses = true
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animationGroup.animations = [theAnimation,theAnimationTranslationX,theAnimationTranslationY]
btnLayer.add(animationGroup, forKey: nil)
}

1- First make the heart shape that will animate a UIView let's name it heartView.
2- self.view.addsubview(heartView) (self.view can be changed to any parent you want)
3- make an invisible button called heartButton and give it the same frame of heartView
4- self.view.addsubview(heartButton) (self.view can be changed to any parent you want)
5- start the heartView animation.
The key point here is not to add the button on the animating view but on their parent (self.view this case) since if the button was on top of the heart and the heart is animating, the button will adopt the animation.
Hope this helps!

Related

Center of CAGradientLayer returns wrong position on rotate animation

I have a circle like in Instagram profile image which has story. I want to have an affect like circle is spinning. For that, I used CABasicAnimation. It is spinning but not in center.
As I searched, I need to give bounty for shapeLayer but when I do that, It doesn't placed where It needs to be.
How can I achieve animation like in Instagram story circle (like circle is spinning)?
EDIT I also try to add "colors" animation but because It works like It is in square, I can't get the desired result.
func addCircle() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: self.bounds.midX, y: self.bounds.midY), radius: self.bounds.width / 2, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 1.0
gradient.frame = circularPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
shapeLayer.path = circularPath.cgPath
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc private func handleTap() {
print("Attempting to animate stroke")
shapeLayer.add(rotationAnimation, forKey: "urSoBasic2")
}
If spinning, why not spin gradient
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = Float.pi * 2
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc func handleTap() {
print("Attempting to animate stroke")
gradient.add(rotationAnimation, forKey: "urSoBasic2")
}
As to off center problem. The correct frame for gradient should be like this :
gradient.frame = self.bounds
To verify the center, you may add a background for the view:
self.background = UIColor.black.
The reason is width and height of the view is not set well due to the constraints. Try to set the correct bounds of the view, so when you add the circularPath, it's center in the view.

CABasicAnimation gets completed when showing another vc modally

I use the following way to pause/resume animation
func pauseAnimation(){
var pausedTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
func resumeAnimation(){
var pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
}
It works like a charm as long as the ViewController is currently presented.
When I present modally another view controller and then dismiss it, animation is done, no matter how much time elapsed during this present/dismiss action.
Do you have any suggestions why this might happen? How to fix it? I'd like to add that all other views holds their state, only the animation is completed.
EDIT:
I just figured out that it happens regardles of pausing/resuming - ongoing animation gets completed as well in such scenario.
Here is my code that shows animation implementation
import Foundation
import UIKit
class CircleView: UIView {
var circleLayer: CAShapeLayer!
#IBOutlet var view: UIView!
#IBOutlet weak var progressLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
Bundle.main.loadNibNamed("CircleView", owner: self, options: nil)
self.addSubview(view)
view.frame = self.bounds
}
override func awakeFromNib() {
super.awakeFromNib()
setupAppearance()
}
func setupAppearance() {
progressLabel.textColor = UIColor.textColor
progressLabel.font = UIFont.textTimerClock
}
func setup(progress:Double, clockwise:Bool) {
self.backgroundColor = UIColor.clear
var strokeColor = UIColor.positiveProgressColor
if !clockwise { strokeColor = UIColor.positiveProgressColor }
// 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: 0.0, endAngle: CGFloat(.pi * 2 * progress), clockwise: clockwise)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = strokeColor.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
layer.addSublayer(circleLayer)
//add grey path
let greyCircleLayer = CAShapeLayer()
let greyCirclePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(.pi * 2.0), clockwise: true)
greyCircleLayer.path = greyCirclePath.cgPath
greyCircleLayer.fillColor = UIColor.clear.cgColor
greyCircleLayer.strokeColor = UIColor.appLightGrey.cgColor
greyCircleLayer.lineWidth = 1.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.insertSublayer(greyCircleLayer, below: circleLayer)
if progressLabel != nil {
progressLabel.text = "10"}
}
func pauseAnimation(){
let pausedTime = circleLayer.convertTime(CACurrentMediaTime(), from: nil)
circleLayer.speed = 0.0
circleLayer.timeOffset = pausedTime
}
func resumeAnimation(){
let pausedTime = circleLayer.timeOffset
circleLayer.speed = 1.0
circleLayer.timeOffset = 0.0
circleLayer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
circleLayer.beginTime = timeSincePause
}
func animateCircle(duration: TimeInterval, color: UIColor) {
// We want to animate the strokeEnd property of the circleLayer
circleLayer.strokeColor = color.cgColor
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: kCAAnimationLinear)
// 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")
}
}
Another thing worth mentioning: init(), awakeFromNib() are not being called again so this is not the case.
Another: The same happens for pushing VC instead of presenting modally.
In general when you display another view controller, the view of current view controller is removed from the window. This will also remove all pending animations from their layers, and any existing animation completion handler is called with false, because the animation is not completed (see also https://stackoverflow.com/a/21200504/2352344).
To continue the animation after returning to the view controller, you should reconstruct the animation object in viewWillAppear.

Auto-starting a strokeEnd Animation

I have an animation for drawing a circle. I combine 3 different circle animations to create a stylised graph/pie chart.
They animate perfection when in an #IBAction from a button. But how would I start the animation automatically when the Storyboard/ViewController is opened/navigated to?
Animation code as follows:
//NF Colour
let colorNF = UIColor(red: 28/255, green: 117/255, blue: 189/255, alpha: 1)
// Create Holding Rectangle and Start and End Angles
let ovalStartAngleNF = CGFloat(-90.01 * .pi/180)
let ovalEndAngleNF = CGFloat(10.0 * .pi/180)
let ovalRectNF = CGRect(origin: CGPoint(x: 122.5,y: 83.5), size: CGSize(width: 100, height: 100))
// Create Bezier Path
let ovalPathNF = UIBezierPath()
// Apply Attributes to Bezier Path
ovalPathNF.addArc(withCenter: CGPoint(x: ovalRectNF.midX, y: ovalRectNF.midY),
radius: ovalRectNF.width / 2,
startAngle: ovalStartAngleNF,
endAngle: ovalEndAngleNF, clockwise: true)
// Styling of the Curve
let progressLineNF = CAShapeLayer()
progressLineNF.path = ovalPathNF.cgPath
progressLineNF.strokeColor = colorNF.cgColor
progressLineNF.fillColor = UIColor.clear.cgColor
progressLineNF.lineWidth = 20.0
progressLineNF.lineCap = kCALineCapRound
// Add Curve to Viewport
self.view.layer.addSublayer(progressLineNF)
// Animated the 'Stroke End' from 0 to 1 over 3 Seconds
let animateStrokeEndNF = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEndNF.duration = 3.0
animateStrokeEndNF.fromValue = 0.0
animateStrokeEndNF.toValue = 1.0
// Add the Animation to the Progress Line
progressLineNF.add(animateStrokeEndNF, forKey: "animate stroke end animation")
As stated by Joe in the comments. The solution was to place my animation code into a viewWillAppear.
override func viewWillAppear(_ animated: Bool) { *ANIMATION CODE* }

How to get an animation inside a view image frame?

I made an animation, but I want to put it in a UIImageView so that I can move it around, clear it or restart it.
This is my code:
#IBOutlet var circle: UIImageView!
// I want to make this inside the circle UIImageView or something like that.
let ovalStartAngle = CGFloat(90.01 * M_PI/180)
let ovalEndAngle = CGFloat(90 * M_PI/180)
let ovalRect = CGRectMake(97.5, 230, 125, 125)
let ovalPath = UIBezierPath()
ovalPath.addArcWithCenter(CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)),
radius: CGRectGetWidth(ovalRect) / 2,
startAngle: ovalStartAngle,
endAngle: ovalEndAngle, clockwise: true)
let progressLine = CAShapeLayer()
progressLine.path = ovalPath.CGPath
progressLine.strokeColor = UIColor.blueColor().CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = 10.0
progressLine.lineCap = kCALineCapRound
self.view.layer.addSublayer(progressLine)
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEnd.duration = 10.0
animateStrokeEnd.fromValue = 0.0
animateStrokeEnd.toValue = 1.0
progressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")

How are these animations typically done in Swift / iOS - See image

What do I need to learn in order to create similar animations to the ones shown in the following picture?
Can some one list all of the technologies involved and possibly a quick process as to how this is done?
For First animation you can use CAShapeLayer and CABasic Animation and animate key path strokeEnd
I builded exactly same you can have look at this link,download and see option Fill circle animation https://github.com/ajaykumar21091/AwesomeCustomAnimations-iOS/tree/master/SimpleCustomAnimations
Edit -
The basic idea here is to draw circle using bezeir path and animate the shapeLayer using CABasicAnimation using keyPath strokeEnd.
override func drawRect(rect: CGRect) {
self.drawBezierWithStrokeColor(circleColor.CGColor,
startAngle: 0,
endAngle: 360,
animated: false)
self.drawBezierWithStrokeColor(self.fillColor.CGColor,
startAngle: 0,
endAngle: (((2*CGFloat(M_PI))/100) * CGFloat(percentage)),
animated: true)
}
//helper methods.
private func drawBezierWithStrokeColor(color:CGColor, startAngle:CGFloat, endAngle:CGFloat, animated:Bool) {
let bezier:CAShapeLayer = CAShapeLayer()
bezier.path = bezierPathWithStartAngle(startAngle, endAngle: endAngle).CGPath
bezier.strokeColor = color
bezier.fillColor = UIColor.clearColor().CGColor
bezier.lineWidth = bounds.width * 0.18
self.layer.addSublayer(bezier)
if (animated) {
let animatedStrokeEnd:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd");
animatedStrokeEnd.duration = (2.0/100)*Double(percentage);
animatedStrokeEnd.fromValue = NSNumber(float: 0.0);
animatedStrokeEnd.toValue = NSNumber(float: 1.0);
animatedStrokeEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
bezier.addAnimation(animatedStrokeEnd, forKey: "strokeEndAnimation");
}
}
private func bezierPathWithStartAngle(startAngle:CGFloat, endAngle:CGFloat) -> UIBezierPath {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius = max(bounds.width, bounds.height)
let arcWidth = bounds.width * 0.25
let path = UIBezierPath(arcCenter : center,
radius : radius/2 - arcWidth/2,
startAngle : startAngle,
endAngle : endAngle ,
clockwise : true)
return path
}

Resources