At this point, I want to put together two things that you already understand individually, but together might hurt your head a little.
Previously we looked at how the order of modifiers matters. So, if we wrote code like this:
Button("Tap Me") {
// do nothing
}
.background(.blue)
.frame(width: 200, height: 200)
.foregroundStyle(.white)
The result would look different from code like this:
Button("Tap Me") {
// do nothing
}
.frame(width: 200, height: 200)
.background(.blue)
.foregroundStyle(.white)
This is because if we color the background before adjusting the frame, only the original space is colored rather than the expanded space. If you recall, the underlying reason for this is the way SwiftUI wraps views with modifiers, allowing us to apply the same modifier multiple times – we repeated background()
and padding()
several times to create a striped border effect.
That’s concept one: modifier order matters, because SwiftUI wraps views with modifiers in the order they are applied.
Concept two is that we can apply an animation()
modifier to a view in order to have it implicitly animate changes.
To demonstrate this, we could modify our button code so that it shows different colors depending on some state. First, we define the state:
@State private var enabled = false
We can toggle that between true and false inside our button’s action:
enabled.toggle()
Then we can use a conditional value inside the background()
modifier so the button is either blue or red:
.background(enabled ? .blue : .red)
Finally, we add the animation()
modifier to the button to make those changes animate:
.animation(.default, value: enabled)
If you run the code you’ll see that tapping the button animates its color between blue and red.
So: modifier order matters and we can attach one modifier several times to a view, and we can cause implicit animations to occur with the animation()
modifier. All clear so far?
Right. Brace yourself, because this might hurt.
You can attach the animation()
modifier several times, and the order in which you use it matters.
To demonstrate this, I’d like you to add this modifier to your button, after all the other modifiers:
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
That will cause the button to move between a square and a rounded rectangle depending on the state of the enabled
Boolean.
When you run the program, you’ll see that tapping the button causes it to animate between red and blue, but jump between square and rounded rectangle – that part doesn’t animate.
Hopefully you can see where we’re going next: I’d like you to move the clipShape()
modifier before the animation, like this:
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.default, value: enabled)
And now when you run the code both the background color and clip shape animate.
So, the order in which we apply animations matters: only changes that occur before the animation()
modifier get animated.
Now for the fun part: if we apply multiple animation()
modifiers, each one controls everything before it up to the next animation. This allows us to animate state changes in all sorts of different ways rather than uniformly for all properties.
For example, we could make the color change happen with the default animation, but use a spring for the clip shape:
Button("Tap Me") {
enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(.default, value: enabled)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.spring(duration: 1, bounce: 0.6), value: enabled)
You can have as many animation()
modifiers as you need to construct your design, which lets us split one state change into as many segments as we need.
For even more control, it’s possible to disable animations entirely by passing nil
to the modifier. For example, you might want the color change to happen immediately but the clip shape to retain its animation, in which case you’d write this:
Button("Tap Me") {
enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(nil, value: enabled)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.spring(duration: 1, bounce: 0.6), value: enabled)
That kind of control wouldn’t be possible without multiple animation()
modifiers – if you tried to move background()
after the animation you’d find that it would just undo the work of clipShape()
.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.