Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chaining animations in SwiftUI

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!

like image 327
Rony Rozen Avatar asked Jul 05 '19 17:07

Rony Rozen


3 Answers

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.

like image 158
marcprux Avatar answered Sep 19 '22 00:09

marcprux


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.  } 
like image 22
Hugo F Avatar answered Sep 18 '22 00:09

Hugo F


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.

like image 30
kontiki Avatar answered Sep 20 '22 00:09

kontiki