Reputation: 3257
I have an app in which I'm trying to animate different properties differently upon change. In the following demonstration app, a spring animation applies to both size and position when the "Flip" button is pressed:
Here is the code:
class Thing: Identifiable {
var id: Int
init(id: Int) {
self.id = id
}
}
struct ContentView: View {
@State var isFlipped: Bool = false
let thing1 = Thing(id: 1)
let thing2 = Thing(id: 2)
var body: some View {
VStack(spacing: 12) {
HStack(spacing: 20) {
ForEach(isFlipped ? [thing2,thing1] : [thing1, thing2]) { thing in
Text("\(thing.id)").font(.system(size: 150, weight: .heavy))
.scaleEffect(isFlipped ? CGFloat(thing.id)*0.4 : 1.0)
.animation(.spring(response: 0.5, dampingFraction: 0.3))
}
}
Button("Flip") { isFlipped.toggle() }
}
}
}
My question is: how can I animate the positions without animating the scale?
If I remove the .scaleEffect()
modifier, just the positions are animated.
But if I then insert it after the .animation()
modifier, then no animation at all occurs, not even the positions. Which seems very strange to me!
I'm familiar with the "animation stack" concept - that which animations apply to which view properties depends on the order in which modifiers and animations are applied to the view. But I can't make sense of where the positions lie in that stack… or else how to think about what's going on.
Any thoughts?
EDITED: I changed the .scaleEffect()
modifier to operate differently on the different Thing objects, to include that aspect of the problem I face; thank you to @Bill for the solution for the case when it doesn't.
Upvotes: 0
Views: 1354
Reputation: 814
This is only one year and five months late, but hey!
To stop the animation of scaleEffect it might work to follow the .scaleEffect
modifier with an animation(nil, value: isFlipped)
modifier.
Paul Hudson (Hacking With Swift) discusses multiple animations and the various modifiers here. You asked about the concepts involved and Paul provides a quick overview.
Alternatively, take a look at the code below. It is my iteration of the solution that Paul suggests.
/*
Project Purpose:
Shows how to control animations for a single view,
when multiple animations are possible
This view has a single button, with both the button's
background color and the clipShape under control
of a boolean. Tapping the button toggles the boolean.
The object is to make the change in clipShape
animated, while the change in background color
is instantaneous.
Take Home: if you follow an "animatable" modifier
like `.background(...)` with an `.animation` modifier
with an `animation(nil)' modifier then that will cancel
animation of the background change. In contrast,
`.animation(.default)` allows the previous animatable
modifier to undergo animation.
*/
import SwiftUI
struct ContentView: View {
@State private var isEnabled = false
var body: some View {
Button( "Background/ClipShape" )
{
isEnabled.toggle()
}
.foregroundColor( .white )
.frame( width: 200, height: 200 )
// background (here dependent on isEnabled) is animatable.
.background( isEnabled ? Color.green : Color.red )
// the following suppresses the animation of "background"
.animation( nil, value: isEnabled )
// clipshape (here dependent on isEnabled) is animatable
.clipShape(
RoundedRectangle(
cornerRadius: isEnabled ? 100 : 0 ))
// the following modifier permits animation of clipShape
.animation( .default, value: isEnabled )
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Upvotes: 0
Reputation: 469
How about scaling the HStack instead of Text?
var body: some View {
VStack(spacing: 12) {
HStack(spacing: 20) {
ForEach(isFlipped ? [thing2,thing1] : [thing1, thing2]) { thing in
Text("\(thing.id)").font(.system(size: 150, weight: .heavy))
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.3))
.scaleEffect(isFlipped ? 0.5 : 1.0)
Button("Flip") { isFlipped.toggle() }
}
}
Upvotes: 2