1

I'm writing a little bit complex animation, which goes in 2 steps:

  1. 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).
  2. 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()
Randex
  • 770
  • 7
  • 30
  • Please show your code, or sufficiently similar / reduced code to make the problem easy to reproduce. – matt May 24 '15 at 14:08
  • Well, which is the one that flashes? Can we eliminate the code for the other animations? – matt May 24 '15 at 15:07
  • `timeView` is the one that flashes. Yes, you can eliminate the other code but that didn't help for me. It still flashes. – Randex May 24 '15 at 15:19
  • Yes, but that's not the reason for eliminating it. The reason for eliminating it is that I need something minimal that I can understand and, if necessary, reproduce in Xcode. Can you pare the example code down some more for me? I don't want to make four extra views just to run your code. – matt May 24 '15 at 15:23
  • Excellent, and I think I now understand the source of the issue; see my answer. – matt May 24 '15 at 15:36

1 Answers1

5

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thank you! But why you can't recommend reusing CABasicAnimations? – Randex May 24 '15 at 15:42
  • `let duration = 0.3` - yeah, I forgot to paste this line when I was editing the question: `alphaAnimation.duration = duration` – Randex May 24 '15 at 15:43
  • "But why you can't recommend reusing CABasicAnimations" It just seems lazy and unnecessary. Plus it's a little risky. It happens that CABasicAnimation is copied when it is assigned to a layer, so your later changes do not affect the previously assigned animations, but that's just an implementation detail. In general a class instance should be treated as a class instance - it is mutable, and mutations affect all reference to it. – matt May 24 '15 at 16:01
  • Thank you for explanation. I have more problems with this flashing. I removed setting alpha from the completion block. Now if I set it right after `addAnimation::` call, `timeView` will be with alpha = 1 from the beginning of the whole animation and it'll animate it from 0 to 1 at the end. If I instead set a delegate for this animation, it'll flash like before. – Randex May 24 '15 at 16:09
  • I believe that what I am saying is right, based on the code you showed. I do not know what changes you made in your code so I don't know what new thing you might be doing wrong. - I suggest that you start all over with a small new project and just prove to yourself that it is possible to animate a view's alpha from 0 to 1 and then remove it, with no intermediate flash. Once you know how to do that, you can go back to your real code and do it there as well. – matt May 24 '15 at 16:19
  • Please take a look at Edit 3 of the question. Also, I've already done alpha animation without flashing before, but this time it's just not working. Maybe because I'm using `beginTime` for sequencing my animations? – Randex May 24 '15 at 16:39
  • Sorry, I'm still confused. You are still referring to two different views: `self.imageView` and `timeView`. Do I need both of those in order to reproduce the issue? – matt May 24 '15 at 16:44
  • Oh, sorry. No, you need only `timeView`. – Randex May 24 '15 at 16:48
  • 1
    Add this line: `alphaAnimation.fillMode = "backwards"` - added that info to my answer. – matt May 24 '15 at 16:58
  • Added working code. This is what I mean by reducing it a small project. That is all the code you need in order to see that this is possible. Now just adapt it to your real situation. This process of eliminating all the extra stuff and concentrating on the actual source of the trouble is the way to debug in real life. – matt May 24 '15 at 17:04
  • Thank you very much! I added this line and it worked, although I don't really understand how this `kCAFillModeBackwards` works. Documentation is not really clear about that. – Randex May 24 '15 at 17:13
  • Well, you were totally right: it's all because of the `beginTime`. We need the `fromValue` to be in force right from the start; that is what the fill mode does. Otherwise, the `fromValue` is not applied until the `beginTime` arrives. – matt May 24 '15 at 17:14