Luke Patterson Luke Patterson - 1 year ago 39
iOS Question

UIView animation doesn't animate on the first try

My issue is that my UIView animation only works after the first time. I have two different sets of animations. The first set works correctly, its the second set that is giving me issues. The first set slides 4 buttons onto the screen a little farther than where I want them. (These locations I call extendedState) The second set of animations slides them a little back to the position that I want. This gives the effect of bouncing back to the right spot.

In between the two animations theres a brief second that will flash the buttons back to their starting position once the first set of animations complete. By setting that starting position to the extendedStates (the location they will be in when they stop animating), the buttons appear not to move between animations (which is good. thats what I want).

Here's the order of operations:

  1. I tap a button that launches the first set of animations (which works perfectly fine)

  2. In the animationDidStop block, I launch the second set of animations (this is the one that doesn't animate on the first try)

Here's the code for the button handler:

@IBAction func selectAnimal(sender: UIButton) {

bigCircle.enabled = false

if(tapToSelectLabel.hidden) {
animator.repositionButtonsToExtendedState(buttons) //sets the starting position to the extendedState so it appears not to move between animations
animator.slideButtonsIntoScreen(buttons, delegate: self) //this is the first set of animations


if(sender.titleLabel != nil) {

if(bigCircleLabel.text != "Choose an animal") {

if let answer:String = bigCircleLabel.text {
solution = game.checkAnswer(answer, question: game.threeQuestions[game.questionIndex])


Now here's the code inside the animationDidStop block

let buttonRects:[CGRect] = [CGRectMake(834, 120, 175, 175),
CGRectMake(631, 198, 175, 175),
CGRectMake(470, 365, 175, 175),
CGRectMake(386, 578, 175, 175)]

UIView.animateWithDuration(0.35, delay: 0, options: [.BeginFromCurrentState, ], animations: {
self.buttons[3].frame = buttonRects[3]
}, completion: { (value:Bool) in
self.buttons[3].enabled = true

UIView.animateWithDuration(0.25, delay: 0, options: [ ], animations: {
self.buttons[2].frame = buttonRects[2]
}, completion: { (value:Bool) in
self.buttons[2].enabled = true

UIView.animateWithDuration(0.4, delay: 0, options: [ ], animations: {
self.buttons[1].frame = buttonRects[1]
}, completion: { (value:Bool) in
self.buttons[1].enabled = true

UIView.animateWithDuration(0.5, delay: 0, options: [ ], animations: {
self.buttons[0].frame = buttonRects[0]
}, completion: { (value:Bool) in
self.buttons[0].enabled = true

^ This code slides the buttons a little backwards to the spot I want them to be in.

Here's the code for my animator class


func slideButtonsIntoScreen(buttons:[UIButton], delegate:UIViewController) {

let center = CGPoint(x:delegate.view.frame.width, y:delegate.view.frame.height)
let startAngles:[CGFloat] = [0.405, 0.75, 1.1, 1.48]
let endAngles:[CGFloat] = [1.95, 2.25, 2.622, 2.98]
let radii:[CGFloat] = [555, 559, 558.5, 551]
let durations:[Double] = [1.75, 1.5, 1.25 , 1]

for index in 0...3 {
let path = UIBezierPath(arcCenter: center, radius: radii[index], startAngle: -startAngles[index], endAngle: -endAngles[index], clockwise: false)
let anim = CAKeyframeAnimation(keyPath: "position")
anim.path = path.CGPath
anim.rotationMode = kCAAlignmentNatural
anim.repeatCount = 0
anim.duration = durations[index] - 0.25
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim.setValue("on", forKey: "type")
anim.delegate = delegate
buttons[index].layer.addAnimation(anim, forKey: "animate position along path"+String(index))
anim.removedOnCompletion = true




func repositionButtonsToExtendedState(buttons:[UIButton]) {

let buttonExtendedRects:[CGRect] = [CGRectMake(755, 155, 175, 175),
CGRectMake(585, 245, 175, 175),
CGRectMake(450, 405, 175, 175),
CGRectMake(393, 590, 175, 175)]

for index in 0...3 {
buttons[index].frame = buttonExtendedRects[index]

I set breakpoints and print statements, so I know for a fact that it reaches the animation on the first try, it just doesn't show the animation. It works exactly as it should every time after the first.

Answer Source

It is typically bad practice to change frames when using auto layout. From what it seems, instead of setting the frame to be wherever the animation stops, you could just set the objects to not reset back to its original position when it completes using this:

anim.fillMode = kCAFillModeForwards
anim.removedOnCompletion = false

but be sure to place those lines BEFORE you add the animation

buttons[index].layer.addAnimation(anim, forKey: "animate position along path"+String(index))

To avoid setting the frames again when you want to move your buttons back, you should be doing basically the same animation but instead of

clockwise: false

set it to true

clockwise: true

then from there you can use your end angles as the new start angles for the movement back, and set your end angles for the movement back to wherever you need the buttons to be