In order to rotate circle in 360 I use CABasicAnimation, in this way :
func startRotate(from : Double) {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = from
rotation.toValue = NSNumber(value: M_PI * 2 - from)
rotation.duration = 10
rotation.isCumulative = true
rotation.repeatCount = 1
self.cdImage.layer.add(rotation, forKey: "rotationAnimation")
}
At the first time the cdImage (view) start from value 0.
When the animation arrived to the middle (after 5 sec) I want to stop the animation and set a transform for the cdImage view, for example return the view 20 degree back and resume it from the new position.
by this way:
1. I call to stopRotate function
func stopRotate(from : Double) {
self.cdImage.layer.removeAnimation(forKey: "rotationAnimation")
}
I set a new transform
self.cdImage.transform = CGAffineTransform(rotationAngle: imageAngle * CGFloat(M_PI) / 180);
call to startRotate with a new value
startRotate(imageAngle * CGFloat(M_PI) / 180)
I suppose that animation will start from the new position and rotate again the view in 360 degree, but actually the animation start the circle from 0 degree, how I can resume the new animation from the new position?
If you want to pick up from where you left off, when you stop the animation, you want to:
capture how much it has rotated (see https://stackoverflow.com/a/9712319/1271826);
determine the in-flight value by examining the presentation layer;
set the transform of the layer accordingly (so that it stays there when you remove the animation); and
remove the animation
Thus:
var angle: CGFloat?
func stopRotate() {
let transform = cdImage.layer.presentation()!.transform
angle = atan2(transform.m12, transform.m11);
cdImage.layer.transform = transform
cdImage.layer.removeAnimation(forKey: "rotationAnimation")
}
You can then start the animation from that angle when you restart it.
Related
I animate the color of CAShapeLayers stored in an Array using CABasicAnimation. The animation displays erratically depending on the animation.duration and I cannot figure out why. I suspect an issue with animation.beginTime = CACurrentMediaTime() + delay
Animation Description
The animation consists in successively flashing shapes to yellow before turning them to black once the animation ends.
Current State of the animation
When the animation duration is above a certain time, it works properly.
For instance with a duration of 2 seconds:
But when I shorten the duration, the result substantially differs.
For instance, with a duration of 1 second:
You will notice that the animation has already cached/ended for the first 10 bars or so, then waits and starts animating the remainder of the shapes.
Likewise, with a duration of 0.5s:
In this case, it seems an even larger number of animation has already ended (shapes are black) before it displays some animation after a certain time. You can also notice that although the shape color animation is supposed to last the same duration (0.5s) some feels quicker than others.
The Code
The animation is called in the viewDidAppear method of the UIViewController class.
I have created a UIView custom class to draw my shapes and I animate them using an extension of the class.
The code to animate the color:
enum ColorAnimation{
case continuousSwap
case continousWithNewColor(color: UIColor)
case randomSwap
case randomWithNewColor(color: UIColor)
case randomFromUsedColors
}
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double){
guard abs(swapColorDuration) != Double.infinity else {
print("Error in defining the shape color change duration")
return
}
let animDuration = abs(duration)
let swapDuration = abs(swapColorDuration)
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration))
switch animationType {
case .continousWithNewColor(color: let value):
var fullAnimation = [CABasicAnimation]()
for i in (0...numberOfSwaps) {
let index = i % (self.pLayers.count)
let fromValue = pLayers[index].pattern.color
let delay = Double(i) * swapDuration / 3
let anim = colorAnimation(for: swapDuration, fromColor: value, toColor: fromValue, startAfter: delay)
fullAnimation.append(anim)
}
for i in (0...numberOfSwaps) {
CATransaction.begin()
let index = i % (self.pLayers.count)
CATransaction.setCompletionBlock {
self.pLayers[index].shapeLayer.fillColor = UIColor.black.cgColor
}
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape")
CATransaction.commit()
}
default:
()
}
}
The segment the whole duration of the animation by the duration of the color change (e.g. if the whole animation is 10s and each shape changes color in 1s, it means 10 shapes will change color).
I then create the CABasicaAnimation objects using the method colorAnimation(for: fromColor, toColor, startAfter:).
func colorAnimation(for duration: TimeInterval, fromColor: UIColor, toColor: UIColor, reverse: Bool = false, startAfter delay: TimeInterval) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "fillColor")
anim.fromValue = fromColor.cgColor
anim.toValue = toColor.cgColor
anim.duration = duration
anim.autoreverses = reverse
anim.beginTime = CACurrentMediaTime() + delay
return anim
}
Finally I add the animation to the adequate CAShapeLayer.
The code can obviously be optimized but I chose to proceed by these steps to try to find why it was not working properly.
Attempts so far
So far, I have tried:
with and without setting the animation.beginTime in the colorAnimation method, including with and without CACurrentMediaTime(): if I don't set the animation.beginTime with CACurrentMediaTime, I simply do not see any animation.
with and without pointing animation.delegate = self: it did not change anything.
using DispatchQueue (store the animations in global and run it in main) and as suspected, the shapes did not animate.
I suspect something is not working properly with the beginTime but it might not be the case, or only this because even when the shapes animate, the shape animation duration seems to vary whilst it should not.
Thank very much in advance to have a look to this issue. Any thoughts are welcome even if it seems far-fetched it can open to new ways to address this!
Best,
Actually there is a relationship between duration and swapColorDuration
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double)
when you call it, you may need to keep this relationship
let colorChangeDuration: TimeInterval = 0.5
animateColors(for: colorChangeDuration * TimeInterval(pLayers.count), .continousWithNewColor(color: UIColor.black), colorChangeDuration: colorChangeDuration)
Also here :
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration)) - 1
This value maybe a little higher than you need.
or
The problem lies in this let index = i % (self.pLayers.count)
if numberOfSwaps > self.pLayers.count, some bands will be double animations.
let numberOfSwaps1 = Int(animDuration / min(swapDuration, animDuration))
let numberOfSwaps = min(numberOfSwaps1, self.pLayers.count)
in the rest is
for i in (0..<numberOfSwaps) {... }
Now if numberOfSwaps < self.pLayers.count. It's not finished.
if numberOfSwaps is larger, It is fine.
If double animations are required, changes the following:
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: nil)
or pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape" + String(i))
I have followed a tutorial on how to create interactive iOS control center animation using UIViewPropertyAnimator:
http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
when swiping up or down the bottom menu, after releasing the finger, I want to add pan velocity to UIViewPropertyAnimator and continue the animation:
popupViewPanned(recognizer:) {
switch recognizer.state {
.
.
.
// after finger released
case .end:
// continue all animations using pan velocity with spring timing
let normalizedPanVelocity: // how to normalize pan velocity
runningAnimators.forEach { $0.continueAnimation(withTimingParameters: spring(for: velocity()), durationFactor: 0) }
}
}
func velocity() -> CGVector {
let pan = panRecognizer
let progress = runningAnimators[0].fractionComplete
let fraction = popupOffset*(1 - progress)
return CGVector(with: pan.velocity(in: view), fraction: fraction)
}
func spring(for velocity: CGVector = .zero) -> UITimingCurveProvider {
return UISpringTimingParameters(dampingRatio: 0.9, initialVelocity: velocity)//UISpringTimingParameters(mass: 2.5, stiffness: 80, damping: 25, initialVelocity: velocity)
}
the problem is when I quickly swipe up or down menu and release the finer, it seems animation hit the wall (slow quickly), then continue to rest
so how can I fix the issue?
I have tried the whole day but I couldn't fix it
The documentation for the UISpringTimingParameters says:
https://developer.apple.com/documentation/uikit/uispringtimingparameters/1649832-init
A vector with a magnitude of 1.0 corresponds to an initial velocity that would cover the total animation distance in one second. For example, if the total animation distance is 200 points and the view’s initial velocity is 100 points per second, specify a vector with a magnitude of 0.5.
Meaning that you have to normalize the velocity using the with of the view.
And looking at the official documentation for CGVector the initializer you are using is confusingly not documented.
https://developer.apple.com/documentation/coregraphics/cgvector
What I've ended up doing was, calculating the normalized vector by myself.
You would need to calculate the total points the view is moving from the start of the animation to the end and then using this distanceToMove to make a "unit vector" from it / normalizing it:
let distanceToMove = newY - oldY
let velocity = recognizer.velocity(in: view)
let relativeVelocityY = velocity.x / distanceToMove
let relativeVelocity = CGVector(dx: 0, dy: relativeVelocityY)
let timing = UISpringTimingParameters(dampingRatio: 0.9, initialVelocity: relativeVelocity)
Let me know if this worked for you.
I'm using Facebook pop framework to perform some cool animations. I'm shaking a button in this way :
let rotation = POPSpringAnimation.init(propertyNamed: kPOPLayerRotation)
rotation.springBounciness = 30
rotation.springSpeed = 20
rotation.velocity = 30.0
rotation.repeatForever = true
button.layer.pop_addAnimation(rotation, forKey: "rotation")
Despite of the repeatForever set to true the animation doesn't repeat. I noticed that if we have the toValue property set, the animation repeats. Am I doing something wrong?
I solved this issue adding the following:
rotation.fromValue = 0.0
You can do it with POPBasicAnimation. If you're rotating forever, you may not need the spring animation.
Looking at your code, you don't have a rotation.toValue You need to tell the animation how far to rotate. Try this:
func configureBtnRotation(btn: UIButton) {
let rotation = POPBasicAnimation(propertyNamed: kPOPLayerRotation)
rotation.toValue = 90.0
rotation.duration = 100.0 //this sets the speed of rotation
rotation.repeatForever = true
button.layer.pop_addAnimation(rotation, forKey: "rotation")
}
Hope this helps.
I have a "U" shaped UIBezierPath which I use as the path for my myImage.layer to animate on. I also have a scrollView. My goal is to have a custom "Pull to Refresh" animation.
The problem I am having is that I want my myImage.layer to update based on how much the scrollView scrolled.
As the scrollView is pulled down, the myImage.layer animates along a "U" shape path. This is the path in my code which I created as a UIBezierPath.
This is how I calculate how far the scrollView is pulled down:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
This is the function to dynamically update the position (it is not working):
func redrawFromProgress(progress: CGFloat) {
// PROBLEM: This is not correct. Only the `x` position is dynamic based on scrollView position.
// The `y` position is static.
// I want this to be dynamic based on how much the scrollView scrolled.
myImage.layer.position = CGPoint(x: progress, y: 50)
}
Basically, this is what I want:
If the scrollView scrolled is 0.0, then the myImage.layer position should be CGPoint(x: 0, y: 0) or the starting point of the path.
If the scrollView scrolled is 0.5 (50%), then the myImage.layer position should be at 50% of the path, I don't know what the CGPoint value would be here.
and so on...
I tried getting the CGPoint values along the UIBezierPath and based on the % of the scrollView scrolled, assign that CGPoint value to it but don't know how to do this. I also looked at this post but I can't get it to work for me.
EDIT QUESTION 1:
By using this extension, I was able to get an array of CGPoints which contain 10 values based on my UIBezierPath:
extension CGPath {
func forEachPoint(#noescape body: #convention(block) (CGPathElement) -> Void) {
typealias Body = #convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, Body.self)
body(element.memory)
}
// print(sizeofValue(body))
let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
CGPathApply(self, unsafeBody, callback)
}
func getPathElementsPoints() -> [CGPoint] {
var arrayPoints : [CGPoint]! = [CGPoint]()
self.forEachPoint { element in
switch (element.type) {
case CGPathElementType.MoveToPoint:
arrayPoints.append(element.points[0])
case .AddLineToPoint:
arrayPoints.append(element.points[0])
case .AddQuadCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
case .AddCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
arrayPoints.append(element.points[2])
default: break
}
}
return arrayPoints
}
I also rewrote the function above called redrawFromProgress(progress: CGFloat) to this:
func redrawFromProgress(progress: CGFloat) {
let enterPath = paths[0]
let pathPointsArray = enterPath.CGPath
let junctionPoints = pathPointsArray.getPathElementsPoints()
// print(junctionPoints.count) // There are 10 junctionPoints
// progress means how much the scrollView has been pulled down,
// it goes from 0.0 to 1.0.
if progress <= 0.1 {
myImage.layer.position = junctionPoints[0]
} else if progress > 0.1 && progress <= 0.2 {
myImage.layer.position = junctionPoints[1]
} else if progress > 0.2 && progress <= 0.3 {
myImage.layer.position = junctionPoints[2]
} else if progress > 0.3 && progress <= 0.4 {
myImage.layer.position = junctionPoints[3]
} else if progress > 0.4 && progress <= 0.5 {
myImage.layer.position = junctionPoints[4]
} else if progress > 0.5 && progress <= 0.6 {
myImage.layer.position = junctionPoints[5]
} else if progress > 0.6 && progress <= 0.7 {
myImage.layer.position = junctionPoints[6]
} else if progress > 0.7 && progress <= 0.8 {
myImage.layer.position = junctionPoints[7]
} else if progress > 0.8 && progress <= 0.9 {
myImage.layer.position = junctionPoints[8]
} else if progress > 0.9 && progress <= 1.0 {
myImage.layer.position = junctionPoints[9]
}
}
If I pull down the scrollView very slow, the myImage.layer actually follows the path. The only problem is that if I pull down on the scrollView very fast, then the myImage.layer jumps to the last point. Could it be because of the way I wrote the if statement above?
Any ideas?
Thanks to #Sam Falconer for making me aware of this:
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
Once I confirmed this, he also helped by mentioning:
Additionally, you will find the CAKeyframeAnimation class to be useful.
With CAKeyfraneAnimation I am able to manually control it's value with this code:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
func redrawFromProgress(progress: CGFloat) {
// Animate image along enter path
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = myPath.CGPath
pathAnimation.calculationMode = kCAAnimationPaced
pathAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
pathAnimation.beginTime = 1e-100
pathAnimation.duration = 1.0
pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
pathAnimation.removedOnCompletion = false
pathAnimation.fillMode = kCAFillModeForwards
imageLayer.addAnimation(pathAnimation, forKey: nil)
imageLayer.position = enterPath.currentPoint
}
Thanks again for the help guys!
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
You may want to try calculating a custom path based on a segment of an arc representing the path between your current position, and your desired position. Basing an animation on this, instead of deconstructing your custom path (which looks very close to just being an arc), may be easier.
CGPathAddArc() with x, y, and r being constant, should get you 90% to what your path is now. You could also get fancier with the path to add that line segment like you have at the beginning of your path. It would just take a bit more work to get the partial path to come out right for all the "I'm at this position, get me a path to this other position" logic.
Additionally, you will find the CAKeyframeAnimation class to be useful. You can feed it a CGPath (perhaps one based on the arc segment to travel), and the timing for the animation, and it can make your layer follow the path.
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGPath/index.html#//apple_ref/c/func/CGPathAddArc
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAKeyframeAnimation_class/index.html
Edit:
Here is some example code for how to draw a partial arc on a CGPath from the current progress to the new progress. I made it work in reverse too. You can play with the numbers and constants, but this is the idea of how to draw an arc segment from a certain percentage to a certain percentage.
Please keep in mind when looking at the CoreGraphics math that it may seem backwards (clockwise vs counterclockwise, etc). This is because UIKit flips everything upside down to put the origin in the upper-left, where CG has its origin in the lower-left.
// start out with start percent at zero, but then use the last endPercent instead
let startPercent = CGFloat(0.0)
// end percent is the "progress" in your code
let endPercent = CGFloat(1.0)
// reverse the direction of the path if going backwards
let clockwise = startPercent > endPercent ? false : true
let minArc = CGFloat(M_PI) * 4/5
let maxArc = CGFloat(M_PI) * 1/5
let arcLength = minArc - maxArc
let beginArc = minArc - (arcLength * startPercent)
let endArc = maxArc + (arcLength * (1.0 - endPercent))
let myPath = CGPathCreateMutable()
CGPathAddArc(myPath, nil, view.bounds.width/2, 0, 160, beginArc, endArc, clockwise)
Here is the full arc segment as defined by the constants minArc and maxArc.
I'm writing a little bit complex animation, which goes in 2 steps:
Change opacity to 0 of UIViews that are not need to be visible and move a UIImageView (which has alpha = 1) to another CGPoint (position).
Change opacity of another UIView to 1 and the opacity of the UIImageView from the previous step to 0, and then after the animation of this step is finished, remove UIImageView from superview.
I've done it this way:
The first step is done without an explicit CATransaction. These 2 animations just have beginTime set to CACurrentMediaTime(). And I'm applying changes to the views right after layer.addAnimation(...) call.
Everything works fine here.
In the second step implementation I call CATransaction.begin() at the beginning.
Inside begin/commit calls to CATransaction I create and add 2 CABasicAnimations to 2 different layers: one for changing the opacity from 0 to 1 (for UIView), and one for changing the opacity from 1 to 0 (for UIImageView). Both animations have beginTime set to CACurrentMediaTime() + durationOfThePreviousStep.
And right after CATransaction.begin() I call CATransaction.setCompletionBlock({...}), and in this completion block I apply changes to these two views: set their new alphas and remove UIImageView from superview.
The problem is, at the end of this whole animation the UIView that has alpha animated to 1 flashes, which means its alpha sets back to 0 (though I've set its alpha to 1 in the completion block) and right after this the completion block executes and its alpha goes up to 1 again.
Well, the question is, how to get rid of this flashing? Maybe this animation can be done in better way?
P.S. I'm not using UIView animations because I'm interested in custom timing functions for these animations.
EDIT 1:
Here's the code. I've deleted UIImageView alpha animation because it's not really necessary.
var totalDuration: CFTimeInterval = 0.0
// Alpha animations.
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
alphaAnimation.beginTime = CACurrentMediaTime()
alphaAnimation.duration = 0.15
let alphaAnimationName = "viewsFadeOut"
view1.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view1.alpha = 0
view2.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view2.alpha = 0
view3.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view3.alpha = 0
view4.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view4.alpha = 0
// Image View moving animation.
// Add to total duration.
let rect = /* getting rect */
let newImagePosition = view.convertPoint(CGPoint(x: CGRectGetMidX(rect), y: CGRectGetMidY(rect)), fromView: timeView)
let imageAnimation = CABasicAnimation()
imageAnimation.keyPath = "position"
imageAnimation.fromValue = NSValue(CGPoint: imageView!.layer.position)
imageAnimation.toValue = NSValue(CGPoint: newImagePosition)
imageAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
imageAnimation.beginTime = CACurrentMediaTime()
imageAnimation.duration = 0.3
imageView!.layer.addAnimation(imageAnimation, forKey: "moveImage")
imageView!.center = newImagePosition
totalDuration += imageAnimation.duration
// Time View alpha.
CATransaction.begin()
CATransaction.setCompletionBlock {
self.timeView.alpha = 1
self.imageView!.removeFromSuperview()
self.imageView = nil
}
let beginTime = CACurrentMediaTime() + totalDuration
let duration = 0.3
alphaAnimation.fromValue = 0
alphaAnimation.toValue = 1
alphaAnimation.beginTime = beginTime
alphaAnimation.duration = duration
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
/* imageView alpha animation is not necessary, so I removed it */
CATransaction.commit()
EDIT 2: Piece of code that cause the problem:
CATransaction.begin()
CATransaction.setCompletionBlock {
self.timeView.alpha = 1
}
let duration = 0.3
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = duration
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
CATransaction.commit()
Maybe the problem is because the timeView has a UITextField and a UIScrollView with 4 subviews. I've tried to replace timeView with a snapshot of timeView (UIImageView), but that didn't help.
EDIT 3:
New code. With this code, timeView always has alpha = 1, and it also animates from 0 to 1.
CATransaction.begin()
CATransaction.setCompletionBlock {
self.imageView!.removeFromSuperview()
self.imageView = nil
}
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = 0.3
alphaAnimation.beginTime = beginTime
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
timeView.alpha = 1.0
CATransaction.commit()
Just looking at your code, I would expect it to flash. Why? Because you have animated timeView's layer's opacity from 0 to 1, but you have not set it to 1 (except in the completion handler, which will happen later). Thus, we animate the presentation layer from 0 to 1 and then the animation ends and it is revealed that the opacity of the real layer was 0 all along.
So, set timeView's layer's opacity to 1 before your animation gets going. Also, since you are using a delayed beginTime, you will need to set your animation's fillMode to "backwards".
I was able to get good results by modifying your test code to be self-contained and to look like this; there is a delay, the view fades in, and there is no flash at the end:
CATransaction.begin()
let beginTime = CACurrentMediaTime() + 1.0 // arbitrary, just testing
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = 1.0 // arbitrary, just testing
alphaAnimation.fillMode = "backwards"
alphaAnimation.beginTime = beginTime
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
timeView.layer.opacity = 1.0
CATransaction.commit()
There are some other things about your code that I find rather odd. It is somewhat risky using a transaction completion block in this way; I don't see why you don't give your animation a delegate. Also, you are reusing alphaAnimation multiple times; I can't recommend that. I would create a new CABasicAnimation for each animation, if I were you.