EP 135 · SwiftUI Animation · Feb 15, 2021 ·Members

Video #135: SwiftUI Animation: The Basics

smart_display

Loading stream…

Video #135: SwiftUI Animation: The Basics

Episode: Video #135 Date: Feb 15, 2021 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep135-swiftui-animation-the-basics

Episode thumbnail

Description

One of the most impressive features of SwiftUI is its animation system. Let’s explore the various flavors of animation, such as implicit versus explicit and synchronous versus asynchronous, to help prepare us for how animation works with the Composable Architecture.

Video

Cloudflare Stream video ID: e119c5839d8e5092f151f4716b05c026 Local file: video_135_swiftui-animation-the-basics.mp4 *(download with --video 135)*

Transcript

0:05

One of the most impressive features of SwiftUI is its animation system. With very little work you can animate almost anything in your application using a simple, declarative API. It’s honestly just an amazing feature of SwiftUI. We can’t say enough good things about it and there’s seemingly no downside to using it.

0:26

Today we are going to begin digging deeper into the SwiftUI animation system with the ultimate goal of seeing how it plays with the Composable Architecture. It turns out that most of SwiftUI’s animation machinery works out of the box for the Composable Architecture with no changes necessary. However, there is one specific situation where they don’t play so nicely, and that is animations that are driven off of the output of effects. It isn’t clear at all how to support animations for this use case, and the solution is really surprising and involves a novel transformation of schedulers.

1:08

But before we can dive into animating asynchronous effects we should maybe start with the basics. There’s a lot of nuance in the animation APIs in SwiftUI, and so we’d like to take a moment to get us all on the same page when it comes to animation. SwiftUI animation

1:25

We’re going to build a little demo application from scratch in order to explore all of the different styles of SwiftUI animations. The demo is heavily inspired by a case study that we have in the Composable Architecture repository, which we can demo real quick:

1:42

If you didn’t already know, the Composable Architecture repo comes with a bunch of demo applications, including this “CaseStudies” application that demonstrates how to solve many real-world problems in a practical manner.

1:56

There are currently 26 case studies in the CaseStudies application. Everything from handling bindings and alerts, to advanced effect handling such as cancellation and long living effects, along with a whole bunch of navigation examples, and finally some examples of higher-order reducers which allow you to layer additional functionality onto existing reducers.

2:18

The case study we are interested in right now is this animation case study. This entire feature is driven off the Composable Architecture. We can tap and drag around to swing the circle around the screen. We can switch the toggle to make the circle grow or shrink in size. And tapping the rainbow causes the circle to cycle through some colors.

2:45

So this demo is quite simple, but it will help us explore all the different flavors that SwiftUI animations come in. Things like implicit versus explicit animations, synchronous versus asynchronous animations, and animated bindings. Implicit animations

3:01

So, let’s start recreating this screen. We’ve got a fresh project ready to go, and in the stub of ContentView that is provided to us we can drop in a Circle view with a specified size: struct ContentView: View { var body: some View { Circle() .frame(width: 50, height: 50) } }

3:26

That puts a black 50x50 circle right in the middle of the screen. Let’s make it so that we can tap on it drag it around. To do this we can attach a DragGesture to the circle: .gesture( DragGesture(minimumDistance: 0).onChanged { value in } )

3:48

This closure will be invoked as you drag around the screen, and the value variable holds a location point which could be used to position the circle.

4:16

In order to capture the gesture’s location and have that value drive the position of the circle we need to introduce some state to this view. SwiftUI comes with a lot of ways to represent state in a view, and each has its pros and cons depending on who owns the state and how state is supposed to flow through the application. The simplest type of state is to just use the @State property wrapper, which gives us some state that can be used locally for this view: @State var circleCenter = CGPoint.zero

4:51

And then we can capture the gesture’s location using this state: .gesture( DragGesture(minimumDistance: 0).onChanged { value in self.circleCenter = value.location } )

5:04

In order for the change in circleCenter to actually affect our little circle view we need to somehow move it. There are a few ways to do this, but perhaps the easiest is to use the .offset view modifier, which allows us to nudge the view in the x- and y-directions away from its natural resting place: .offset(x: self.circleCenter.x, y: self.circleCenter.y)

5:32

Now we can tap and drag the circle to anywhere on the screen. One strange thing is that after tapping on the circle it instantly snaps to a position that is below and to the right of where the tap happened. To correct for this we need to shift the circle up and to the left by half the size of the circle: .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25)

6:06

So this is already pretty cool. It was so easy to get something interact in our SwiftUI preview.

6:12

But now we can add some magic. With one single line of code we can make it so that the circle floats to our touch in a continuous animation, rather than instantly following our touch: Circle() .frame(width: 50, height: 50) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .animation(.default) .gesture( DragGesture(minimumDistance: 0).onChanged { value in self.circleCenter = value.location } )

6:34

With that one change we can drag our touch around the screen and the circle will follow it with a slight lag. We can try out different types of animations, such as .linear or easing animations: .animation(.linear) .animation(.easeIn)

6:58

If we want to get really fancy we can use a spring animation, which will allow us to fling the circle around the screen: .animation(.spring(response: 0.3, dampingFraction: 0.1))

7:12

That’s really fun.

7:16

Let’s animate another aspect of this circle. We’ll add a toggle so that when it’s switch on it will scale the circle up, and when it’s switch off it will scale back down to its original size. Just as we did with the circle’s center, we need to hold some extra state to represent whether or not the circle is scaled: @State var isCircleScaled = false

7:40

And then we can use the .scaleEffect view modifier to scale the circle up or down depending on this boolean: Circle() .frame(width: 50, height: 50) .scaleEffect(self.isCircleScaled ? 2 : 1) …

7:55

Next we’ll add the toggle to the screen by first wrapping the circle in a VStack so that we can put the toggle under the circle: VStack { Circle() … }

7:59

And then we can insert a toggle into this stack by providing a binding that is derived from our new boolean state: Toggle("Scale", isOn: self.$isCircleScaled)

8:27

And just like that we can flip the toggle on and the circle will grow in size with a spring animation.

8:44

It’s worth noting that the order that the .offset and .scaleEffect modifiers is applied is important. If we switched the order: //.scaleEffect(self.isCircleScaled ? 2 : 1) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .scaleEffect(self.isCircleScaled ? 2 : 1)

8:56

Then the circle would be scaled relative to the center of the screen, rather than the center of the circle. That’s not what we want, so let’s go back to how we had it.

9:05

So, this is pretty interesting. The .animation modifier doesn’t just animate whatever modifier it’s tacked onto, but rather it seems to animate everything in the view hierarchy. The documentation isn’t super clear on how exactly this works, but there are some hints: Note Applies the given animation to all animatable values within this view. Use this modifier on leaf views rather than container views. The animation applies to all child views within this view; calling animation(_:) on a container view can lead to unbounded scope.

9:22

So it seems it’s best practice to apply this modifier as close to the actual view we want to animate as possible, not to container views, as it seems to animate everything in the view.

9:54

Let’s layer on one last animation just so that we have a lot of variety of animations in our demo. We’re going to add a button such that when you press the button, the circle will cycle through a bunch of colors, before eventually going back to black.

10:08

The only way to change the circle’s color is to introduce new state that represents its current color: @State var circleColor = Color.black

10:23

And we can fill the circle with that color: Circle() .fill(self.circleColor) …

10:28

The button to cycle the colors can be added to the bottom of the VStack : Button("Cycle colors") { }

10:33

Let’s try something simple to start. We’ll just set the color of the circle to red when this button is tapped: Button("Cycle colors") { self.circleColor = .red }

10:43

Tapping this button now causes something pretty funky. The color of the circle changes, but it does so with a springy animation. That’s a little strange, and we probably don’t want to do that, but let’s continue with our goal of having the circle cycle through many types of colors.

11:09

The most straightforward way to do this would be to us DispatchQueue.main.async . For example, after setting the color to red, we can wait for one second and then set it to blue: self.circleColor = .red DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .blue }

11:31

Then we could wait one more second and change it to green: self.circleColor = .red DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .blue DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .green } }

11:43

We could wait one more second and change it to purple: self.circleColor = .red DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .blue DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .green DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .purple } } }

11:47

Before finally waiting one more second and changing back to black: self.circleColor = .red DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .blue DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .green DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .pink DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .black } } } }

11:50

The color cycle feature now works, albeit still with the funky spring animation, but this code is pretty gnarly as is. One thing we could do is flatten all the nested async calls by increasing each delay amount: Button("Cycle colors") { self.circleColor =.red DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.circleColor = .blue } DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.circleColor = Color.green } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.circleColor = Color.pink } DispatchQueue.main.asyncAfter(deadline: .now() + 4) { self.circleColor = Color.black } }

12:33

That looks a little nicer, but it’s annoying that we now have to keep the delay amounts in sync, and if we added another color we would need to make sure the delays are sequential.

12:48

We could also get rid of the copy-pasted code and do everything at once in a loop: Button("Cycle colors") { [ Color.red, .blue, .green, .pink, .black ] .enumerated() .forEach { offset, color in DispatchQueue.main.asyncAfter( deadline: .now() + .seconds(offset) ) { self.circleColor = color } } }

14:00

That prevents the delays from getting out of sync, but it’s also a pretty intense piece of code. It is also a little dangerous to kick off all of these delayed units of work at once like this, because .asyncAfter with a timed delay is not an exact science. The work can be executed a small amount of time after the time you specify, and the more of these pieces of work you queue up the more likely they are to get out of sync. However, for simple applications such as this demo it’s probably ok to do this.

14:44

With the basics of the demo in place, let’s address this weird issue we are seeing where the colors are animated with a spring animation. We’d prefer if the circle color did not animate with a springy animation.

14:54

Luckily we can chain on multiple .animation view modifiers in order to change the type of animation that is used at various points in the chain. So, if we tack on a .animation(.linear) just below the .fill modifier we will animate that with a linear animation while retaining springiness for the rest of the modifiers: Circle() .fill(self.circleColor) .animation(.linear) .frame(width: 50, height: 50) .scaleEffect(self.isCircleScaled ? 2 : 1) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .animation(.spring(response: 0.3, dampingFraction: 0.1))

15:23

So that’s pretty cool. Targeted implicit animation

15:33

Adding animation to a SwiftUI view via the .animation view modifier is known as an “implicit” animation. It’s an un-targeted, wide reaching way of performing animation in a view because it will animate all animatable properties that change in the view hierarchy. You don’t get to target just a small subset.

16:01

This means that understanding exactly how this modifier does the things it does can feel magical and mysterious sometimes.

16:12

What if we wanted to take these implicit animations and be more precise with how they work and make them a bit more easy to reason about?

16:31

For example, suppose we don’t want to animate the scale with a spring anymore, and instead we want a simple easing animation. We would hope we could just do: .scaleEffect(self.isCircleScaled ? 2 : 1) .animation(.easeInOut)

16:48

When we run this we see that indeed the scale now eases, but when we drag we see that the offset is also now animated with easing instead of the spring. That seems really strange, especially since we have the spring animation after the .offset modifier. Somehow the .easeInOut animation is overriding it even though it comes before it. While it seems that usually the .animation modifier can animate the properties specified higher up in the chain, it doesn’t seem to always be the case, as we are seeing here with the .easeInOut animation affecting offset, which happens further down the chain.

17:34

Our only guess as to why this is happening is that perhaps both .offset and .scaleEffect are simply changing the same underlying transformation of the view, and so perhaps the implicit animation bundles both of those changes into a single transaction. But that’s just a guess, and either way it seems like a bug.

17:53

Things get even stranger if you consider the ways that SwiftUI allows you to disable animations in a chain. The .animation modifier allows you to pass nil to it to represent that you want to disable animations. For example, say we want to disable animations entirely for the scaling effect, but leave it in place for the offset we could try: .scaleEffect(self.isCircleScaled ? 2 : 1) .animation(nil) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .animation(.spring(response: 0.3, dampingFraction: 0.1))

18:24

But this unfortunately has a similar problem as before, where animation is disabled both for scaling and offset.

18:46

Again, this is most likely a bug, but also highlights the trickiness of dealing with implicit animations, where under the hood it is trying to animate everything in the view hierarchy and so stacking on multiple animations that animate related properties can cause things to get mixed up.

19:02

We can work around this bug by using an overload of the .animation modifier, which allows you to specify an additional Equatable value that restricts the animation to be applied only when the value changes. So, if we do a little bit of extra work we can make sure that the scale animation is only applied when isCircleScaled changes, and the offset is animated only when circleCenter changes: .scaleEffect(self.isCircleScaled ? 2 : 1) .animation(nil, value: self.isCircleScaled) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .animation( .spring(response: 0.3, dampingFraction: 0.1), value: self.circleCenter )

19:44

Now it behaves as we expect. The scale does not animate at all, but the offset still animates.

19:54

It’s worth noting that the role the optional places in the .animation modifier is a little different from other SwiftUI modifiers. Most other modifiers that take an optional as an argument use nil to mean “use whatever the default is.” There’s a bunch of these kinds of modifiers, such as .accentColor , .font , .preferredColorScheme and .foregroundColor . For example, if you use pass nil to the .foregroundColor modifier then it effectively means “don’t change the foreground color”: .foregroundColor(nil)

20:31

This allows you to put logic in this argument where sometimes you may want to override the foreground color and sometimes you may not want to: .foregroundColor(self.isCircleScaled ? .red : nil)

20:50

This is extremely important to how SwiftUI keeps track of how a view is structured so that it knows what to animate. By having this single view modifier SwiftUI knows that the foreground color may need to animate from time-to-time. If this API was not designed this way you would be forced to do an if / else statement so that you can conditionally apply the .foregroundColor modifier. But doing so completely destroys the information SwiftUI needs in order to figure out that you only want to animate the foreground color.

21:21

It’s very easy to see this why this is problematic. Let’s hold our circle view in a variable so that we can perform some conditional modifications: let circle = Circle() …

21:30

And then let’s do a conditional to change the foreground color depending on if the circle is scaled or not: if self.isCircleScaled { circle.foregroundColor(.red) } else { circle }

21:57

Let’s also revert some of the animation changes we made a minute ago by removing the .animation(nil) from the scale effect, and going back to using a spring animation for both the scale and offset: .scaleEffect(self.isCircleScaled ? 2 : 1) .offset(x: self.circleCenter.x - 25, y: self.circleCenter.y - 25) .animation(.spring(response: 0.3, dampingFraction: 0.1))

22:12

We would hope that this works basically as before, where dragging the circle and switching the toggle causes the circle to animate with a spring animation. However, only the drag gesture causes an animation. The toggle does not change the circle with an animation. It happens instantaneously.

22:31

The reason is that SwiftUI does not know that we mean for this conditional to represent the same view, just with different modifiers. It has no choice but to assume that executing these branches of the conditionals correspond to removing an existing view and putting in a new view. So it has no hopes of animating these changes.

22:50

On the other hand, if we get rid of the conditional and bring back the inline .foregroundColor : // let circle = Circle() … .foregroundColor(self.isCircleScaled ? .red : nil) // if self.isCircleScaled { // circle // .foregroundColor(.red) // } else { // circle // }

23:04

Animation is now working properly. We can even throw in a little text view inside the circle so that we can see the foreground color changing: Circle() .fill(self.circleColor) .overlay(Text("Hi")) …

23:23

So, we see that passing nil to some SwiftUI view modifiers has the meaning of “use the default”, which handy for keeping a view intact so that it can be properly animated.

23:33

However, passing nil to the .animation modifier is quite different in that it means to disable animations completely. There is no easy way to do some logic inline so that you can decide between animating in some specific manner or just using whatever the default is. It would be nice to have a better way to distinguish between these use cases, perhaps something like this: .animation(.disabled)

23:59

But alas that’s not what SwiftUI gives us today, and so it’s important to keep these distinctions in mind.

24:04

Before moving on, let’s add one additional small feature. Let’s add a button that will reset the UI back to its default state. We can add a button and then reset the three @State fields back to their default values: Button("Reset") { self.circleCenter = .zero self.circleColor = .black self.isCircleScaled = false }

24:35

When we run this we will see it animates back to its default state, springy animation and all. What if we wanted to reset the state with a different animation? Or no animation at all?

24:53

This is tricky with implicit animations. They operate at such a high level and with such broad coverage that it can be difficult to insert little bits of logic to customize their behavior. It’s not entirely clear how we can accomplish this. We could try introducing some new state: @State var isResetting = false

25:19

So that our animation could be conditional based on it: .animation( self.isResetting ? nil : .spring(response: 0.3, dampingFraction: 0.1) )

25:30

And then maybe we’d hope we could just do this in the reset button action: Button("Reset") { self.isResetting = true self.circleCenter = .zero self.circleColor = .black self.isCircleScaled = false self.isResetting = false }

25:45

However this doesn’t work because all of these mutations are batched into a single transaction, and so we never get an opportunity to see that isResetting is true for a brief moment of time in the view’s body.

26:11

We could also add a small delay, and that does work but feels very hacky: Button("Reset") { self.isResetting = true self.circleCenter = .zero self.circleColor = .black self.isCircleScaled = false DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) { self.isResetting = false } } Explicit animations

26:41

That’s a quick introduction to implicit animations. They can give you some pretty impressive results with very little work. They are mostly an un-targeted way of performing animations since they typically animate everything in the view. But they can also be pretty difficult to debug. We’ve personally spent quite a bit of time fiddling with animation parameters and the order of chaining to get things to look the way we expect, and often we don’t have a ton of confidence that we’ve done things the right way.

27:20

Well, if there’s something called “implicit” animations, then it probably means there’s also something called “explicit” animations, and perhaps those solve some of these problems?

27:29

It does indeed. Where implicit animations will under the hood animate everything in the view when state changes, explicit animations instead require you to say what kind of animation should be used at the time of mutating state. This allows you to be more targeted with what should animate and what shouldn’t.

27:52

Let’s start by commenting out all the .animation modifiers we are using right now: // .animation(.linear) … // .animation(.spring(response: 0.3, dampingFraction: 0.1))

27:59

This has removed all animation from our preview.

28:07

To animate a state change with explicit animations you wrap the mutation in a function called withAnimation . This function does the work of figuring out what needs to be animated after the state change happens: .gesture( DragGesture(minimumDistance: 0).onChanged { value in withAnimation { self.circleCenter = value.location } } )

28:47

This function should be reminiscent of the UIView.animate function, which will automatically animate whatever view changes you make inside a block: UIView.animate(withDuration: 1) { label.transform = .init(translateX: 200, y: 200) }

29:03

With this change we now have a drag animation back. It’s just the default animation, and so if we want the springiness back too we have to pass an Animation value to withAnimation : withAnimation(.spring(response: 0.3, dampingFraction: 0.1)) { self.circleCenter = value.location }

29:29

So, rather than saying we want to animate the offset of the circle no matter how the circleCenter field is mutated, we are saying that when we want to animate this very specific mutation of circleCenter . It’s a far more targeted approach than the implicit animation style, and with that change the swinging animation works again when dragging the circle.

29:57

We can do something similar when we change circleColor : withAnimation(.linear) { self.circleColor = color }

30:11

Now tapping on “cycle color” causes the circle to change colors just as before.

30:20

The only animation left to handle is the scaling animation. This one is a little different from the animations because the mutation made is done via a binding: Toggle("Scale", isOn: self.$isCircleScaled)

30:35

We don’t mutate the state directly. How are we supposed to wrap the mutation in a withAnimation block?

30:54

One interesting thing about the withAnimation function is that it does not return Void like we might expect. The closure we provide can return a generic result, and then withAnimation returns a generic result: func withAnimation<Result>( _ animation: Animation? = .default, _ body: () throws -> Result ) rethrows -> Result

31:17

If we clean up this signature a bit we see that at it’s core it has the following shape: // withAnimation<Result> = (() -> Result) -> Result

31:37

We should clean this up further, though, because Result is a generic and not the result type: // withAnimation<R> = (() -> R) -> R

31:51

This is a special case of a much more general concept known as a “continuation.” We’ve come across another special case of continuations on Point-Free many times in the past. It had the following shape: // Async<A> = ((A) -> Void) -> Void

32:22

This shape expresses the concept of asynchrony, allowing you to hand a closure off to someone else so that they can invoke it and provide you data when it’s ready.

32:35

Both of these shapes are special cases of the far more general continuation signature: ((A) -> R) -> R

32:48

This one single signature expresses the essence of how one hands of execution to a 3rd party and how they return execution back to you. It’s a very powerful concept that subsumes many seemingly disparate concepts, and we will have more to say about this in future Point-Free episodes.

33:03

But with that said, perhaps all we need to do is wrap the binding in one of these withAnimation continuations: isOn: withAnimation { self.$isCircleScaled })

33:24

Unfortunately, it does not animate.

33:31

One thing we could do is create the Binding from scratch rather than use the convenience API that SwiftUI gives us. A Binding consists of a getter and a setter, which can be done by accessing the isCircleScaled field under the hood: Toggle( "Scale", isOn: Binding.init( get: { self.isCircleScaled }, set: { isCircleScaled in self.isCircleScaled = isCircleScaled } ) // self.$isCircleScaled )

34:12

Now that we have taken back control over how this binding is constructed we have the opportunity to wrap the setter in a withAnimation block: Toggle( "Scale", isOn: Binding.init( get: { self.isCircleScaled }, set: { isOn in withAnimation(.spring(response: 0.3, dampingFraction: 0.1)) { self.isCircleScaled = isOn } } ) // self.$isCircleScaled )

34:33

Now when we run our preview we will see that the circle animates when it is scaled.

34:35

It would be a little sad if we had to forgo SwiftUI’s niceties, such as deriving bindings from state, just because we want to animate the binding changes. Luckily SwiftUI provides a way to make bindings animate automatically. The Binding type has its own .animation method on it Toggle( "Scale", isOn: self.$isCircleScaled.animation(.spring(response: 0.3, dampingFraction: 0.1)) // Binding.init( // get: { self.isCircleScaled }, // set: { isCircleScaled in // withAnimation(.spring(response: 0.3, dampingFraction: 0.1)) { // self.isCircleScaled = isCircleScaled // } // } // ) )

35:06

And just like that any change to the binding will be animated with a springy animation.

35:14

It’s worth mentioning that even though we are using a .animation method that this is still considered an explicit animation and not an implicit animation. This is because we are still targeting the animation to only affect parts of the view that change when isCircleScaled changed this specific switch is toggled.

35:34

Now the really cool thing about using the explicit style of animations is that we can implement the reset button in a very straighforward manner. Since we don’t want to perform any animations we can just make the mutations directly: Button("Reset") { self.circleCenter = .zero self.circleColor = .black self.isCircleScaled = false }

36:06

If we did want to perform an animation we can just wrap it in withAnimation : withAnimation { self.circleCenter = .zero self.circleColor = .black self.isCircleScaled = false }

36:22

And if we wanted to only animate a subset of the properties we could do that easily too: withAnimation { self.circleCenter = .zero self.circleColor = .black } self.isCircleScaled = false

36:39

This is starting to show the power behind explicit animations. They allow you to be far more targeted in what you want to animate and how you want to animate.

36:48

It’s also worth noting that SwiftUI used to more heavily lean on implicit animations. For example, it used to be that if you ever made a change to the data source powering a List view then those changes would automatically animate. That would cause rows to animate into place or slide away when removed. That made for a really nice demo since you could get animation basically for free, but also meant that a lot of really strong opinions were hardcoded directly in SwiftUI’s foundational components, which seems strange. However, in iOS 14 and Xcode 12 that behavior was changed so that you had to start using explicit animations in order to animate a List view. So it appears that SwiftUI is heading more towards favoring explicit animations over implicit animations.

37:32

So, that’s the basics of SwiftUI animations. They come in two major flavors: implicit and explicit. Implicit is heavily state driven, in that whenever state changes it will automatically animate all changes to the view based on that state change. You don’t have the ability to perform animations when an event occurs, it only happens when state changes. This is why it was so hard to prevent animations when the reset button was tapped. Because that is an event and implicit animations don’t handle events well.

38:02

On the other hand, explicit animations are far more targeted and more event driven. This means that when events occur we can tell SwiftUI to animate a state change. This is why it was so easy to opt in or out of animation when the animation button was tapped. Next time: Animation in the Composable Architecture

38:23

So now that we understand the basics of SwiftUI animations, let’s see how the Composable Architecture integrates with animation.

38:30

Most of our viewers probably already know this, but just in case, the Composable Architecture is a library that we opened sourced in May of 2019 after having built it from first principles in the episodes of Point-Free. It is focused on solving a few core problems that we think any architecture should solve, such as composability, modularity, testability, dependencies and more.

38:55

Nearly everything we’ve done so far with animations works just fine with the Composable Architecture, but there are a few rough edges that need to be smoothed out. And in the process of smoothing out those edges we will come across a really amazing application of transforming schedulers…next time! Downloads Sample code 0135-swiftui-animation-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .