34

I'm working on a relatively complex animation in SwiftUI and am wondering what's the best / most elegant way to chain the various animation phases.

Let's say I have a view that first needs to scale, then wait a few seconds and then fade (and then wait a couple of seconds and start over - indefinitely).

If I try to use several withAnimation() blocks on the same view/stack, they end up interfering with each other and messing up the animation.

The best I could come up with so far, is call a custom function on the initial views .onAppear() modifier and in that function, have withAnimation() blocks for each stage of the animation with delays between them. So, it basically looks something like this:

func doAnimations() {
  withAnimation(...)

  DispatchQueue.main.asyncAfter(...)
    withAnimation(...)

  DispatchQueue.main.asyncAfter(...)
    withAnimation(...)

  ...

}

It ends up being pretty long and not very "pretty". I'm sure there has to be a better/nicer way to do this, but everything I tried so far didn't give me the exact flow I want.

Any ideas/recommendations/tips would be highly appreciated. Thanks!

Rony Rozen
  • 3,957
  • 4
  • 24
  • 46

3 Answers3

41

As mentioned in the other responses, there is currently no mechanism for chaining animations in SwiftUI, but you don't necessarily need to use a manual timer. Instead, you can use the delay function on the chained animation:

withAnimation(Animation.easeIn(duration: 1.23)) {
    self.doSomethingFirst()
}

withAnimation(Animation.easeOut(duration: 4.56).delay(1.23)) {
    self.thenDoSomethingElse()
}

withAnimation(Animation.default.delay(1.23 + 4.56)) {
    self.andThenDoAThirdThing()
}

I've found this to result in more consistently smoother chained animations than using a DispatchQueue or Timer, possibly because it is using the same scheduler for all the animations.

Juggling all the delays and durations can be a hassle, so an ambitious developer might abstract out the calculations into some global withChainedAnimation function than handles it for you.

marcprux
  • 9,845
  • 3
  • 55
  • 72
  • 2
    Does this still work for you? It's not working for me on iOS 16. Posted a question with example here: https://stackoverflow.com/questions/73993064/how-can-i-chain-multiple-animations-to-show-then-hide-a-view-using-withanimation – gohnjanotis Oct 08 '22 at 14:22
  • Is the delay guaranteed? I don't feel safe working with hard-coded delays for animations – marticztn Feb 13 '23 at 02:55
  • I did not find that the delay portion was working in iOS 16. I substituted with a `DispatchQueue.main.asyncAfter(deadline: .now() + x.xx)` and this worked. – C6Silver Mar 18 '23 at 03:38
5

Using a timer works. This from my own project:

@State private var isShowing = true
@State private var timer: Timer?

...

func askQuestion() {
    withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
        isShowing.toggle()
    }
    timer = Timer.scheduledTimer(withTimeInterval: 1.6, repeats: false) { _ in
        withAnimation(.easeInOut(duration: 1)) {
            self.isShowing.toggle()
        }
        self.timer?.invalidate()
    }

    // code here executes before the timer is triggered.

}
Hugo F
  • 620
  • 6
  • 11
3

I'm afraid, for the time being, there is no support for something like keyframes. At least they could have added a onAnimationEnd()... but there is no such thing.

Where I did manage to have some luck, is animating shape paths. Although there aren't keyframes, you have more control, as you can define your "AnimatableData". For an example, check my answer to a different question: https://stackoverflow.com/a/56885066/7786555

In that case, it is basically an arc that spins, but grows from zero to some length and at the end of the turn it progressively goes back to zero length. The animation has 3 phases: At first, one end of the arc moves, but the other does not. Then they both move together at the same speed and finally the second end reaches the first. My first approach was to use the DispatchQueue idea, and it worked, but I agree: it is terribly ugly. I then figure how to properly use AnimatableData. So... if you are animating paths, you're in luck. Otherwise, it seems we'll have to wait for the possibility of more elegant code.

kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Thanks for the quick response. Let me ask you this - if you needed to take your animation from the other question, stop the animation when it completes a full circle, then fade out and back in and re-start the animation. Can you think of a way to do that without adding DispatchQueue? – Rony Rozen Jul 05 '19 at 19:39
  • From the top of my head, I cannot think of a way :-( I did try setting two animations a the same time, and one of them with animation().delay() so it would start when the first one was done. But I abandoned the idea, as I wasn't being very successful. But maybe that is something you can explore. – kontiki Jul 05 '19 at 19:50
  • 1
    Yeah, I tried that too with not much luck... :[ Thanks again and good luck to us all... :] – Rony Rozen Jul 05 '19 at 19:52