UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Controlling the animation stack

Paul Hudson    @twostraws   

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: order modifier 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().

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.