Anton
Anton

Reputation: 3257

SwiftUI - Animating only view positions

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:

Screen recording of app

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

Answers (2)

Bill Nattaner
Bill Nattaner

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

Bill
Bill

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

Related Questions