TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Animation State: How to stop button from changing size at end?

Forums > SwiftUI

Hi! I am trying to animate a button:

struct ButtonView: View {
    @State private var isPressed = false

    var label: String
    let action: () -> Void

    var body: some View {

            Text(label)
                // There are lots of customizations here; no need to put them here
                .onTapGesture {
                    isPressed = true
                    action()
                }
                .scaleEffect(isPressed ? 1.25 : 1.0)
                .animation(.easeInOut.repeatCount(2, autoreverses: true), value: isPressed)

    }

}

This is going to create an animation that will scale out the button and then scale it back to its original size after one simple tap. In addition, it performs a custom action called action(), but that isn't really useful, I just want to be clear.

The problem is when I test this and tap the button, it runs very close to what you would expect, but with an additional and unwanted part. When I tap the button, it becomes large and then its original size and then large again, but then it jumps back with no animation to the large size again.

I believe that SwiftUI is trying to keep all of the state updated (isPressed), as it starts out false, becomes true when button is tapped, evaluates the ternary in .scaleEffect to be 1.25, which makes it larger. It then reverses this animation to make it small again... but then it must make the size larger again (1.25) because... why? I feel like I am getting confused here.

How can I make the button simply get larger and then go back to its original size? What am I misunderstanding here? Please feel free to let me know if there are any clarifications needed. I appreciate the help! :)

Hint: Realize that this isn't a SwiftUI Button type, but rather just a custom shape that I made.

2      

One thing to remember about variables. When you set a variable, it is instantly set. That is, when you are animating a scale from 1.0 up to 1.25 the variable instantly jumps to 1.25. It does NOT change from 1.0 to 1.1, then to 1.15, then to 1.20, then to 1.21, etc.

So when you set isPressed to true, this triggers the scaleEffect to be 1.25, instantly! But because you added an animation sequence, SwiftUI takes a before snapshot and an after snapshot. Then it goes through the long maths calculations to create smooth animations from the before state to the after state and spreads the animation out over your specified time frame.

But in your case, you specified TWO animations: small to big, then big back down to small. SwiftUI smoothly animated this for you. But your END state was a big size. So SwiftUI was obligated to draw the final view to your precise specifications. That is, the button label had to be BIG.

Since the animation ended in a small state, but you declared the view to be a big state, SwiftUI abruptly changed the text view to be the final large size without the smooth animation.

Copy into Playgrounds and give this a spin.

import SwiftUI
import PlaygroundSupport

struct ButtonView: View {
    @State private var isPressed = false
    var myLabel: String // This is the button's label

    var body: some View {
        VStack{
            Text("Scale Effect").padding(.bottom)
            Button(myLabel) { isPressed.toggle() }
                .buttonStyle(.borderedProminent)
                .scaleEffect(isPressed ? 1.25 : 1.0) // this is the END STATE
                 // THREE animation steps
                 // 1. small to big
                 // 2. big   to small
                 // 3. small to big
                .animation(.easeInOut.repeatCount(3, autoreverses: true), value: isPressed)
        }.frame(width: 150, height: 150)
    }
}

PlaygroundPage.current.setLiveView(ButtonView(myLabel: "Tap Me"))

2      

Hi! Unfortunately, I am away from my computer and cannot test this. However, it looks like this will make the button end up in a bigger size than the original size, but have a smooth animation. I have tried doing just that, but my goal is to create a button that ends up in the original size: you tap the button once, it becomes large and then back to its original size, and then the action happens.

I found a person who happened to have the same question as me, but have not had time to look at the answers yet (I just want to clarify and respond to you in a timely manner):

https://stackoverflow.com/questions/57252706/animations-triggered-by-events-in-swiftui

I will take a look at everything in more detail within the next few hours, thank you for your response!

2      

Try this variation on a theme. I added a short delay that resets the isPressed state variable.

Also, I am a fan of computed properties. It's a way to positively declare your design intentions.

Copy into Playgrounds and give this a spin.

import SwiftUI
import PlaygroundSupport

struct ButtonView: View {
    @State private var isPressed = false
    var buttonScale: Double { isPressed ? 1.35 : 1.0 }  // Declare your button scale
    var myLabel:     String // This is the button's label

    func toggleAfterShortDelay() -> Void {
        // This triggers the animation.
        isPressed.toggle()     // set end state to BIG

        let delay = 0.75 // <-- Try different delay values
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            // This ALSO triggers the animation.
            isPressed = false  // set end state to SMALL
        }
    }

    var body: some View {
        VStack{
            Text("Scale Effect").padding(.bottom)
            Button(myLabel) { toggleAfterShortDelay() }
                .buttonStyle(.borderedProminent)
                .scaleEffect( buttonScale ) // this is the END STATE
                 //Animate from one size to the other
                .animation(.easeInOut, value: isPressed)
        }.frame(width: 150, height: 150)
    }
}

PlaygroundPage.current.setLiveView(ButtonView(myLabel: "Tap Me"))

2      

Oh! I didn't think of using DispatchQueue! Thank you, this is a great solution! I ended up making some changes but here is what I ultimately did:

struct ButtonView: View {
    @State private var isPressed = false

    var label: String
    let action: () -> Void

    var body: some View {

            Text(label)
                // There are lots of customizations here; no need to put them here
                .onTapGesture {
                    isPressed.toggle()
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                        isPressed.toggle()
                    }
                    action()
                }
                .scaleEffect(isPressed ? 1.25 : 1.0)
                .animation(.easeInOut, value: isPressed)

    }

}

A couple of thoughts:

  • I used .toggle() everywhere because you can now simply make isPressed false or true and thus make the button animation go from small > big > small or big > small > big!
  • I didn't add any properties to the struct, although I could add a delay and scale property, perhaps set to default values. The point of my ButtonView struct is to make consistent buttons across my entire project, so I didn't really think that would actually do much good.
  • How would I go about making this happen for a "touch down" event? Where I press the button and hold it within the bounds of the button, and it would get larger and then get smaller once I am either not pressing the screen or dragged outside of the bounds of the button? I was briefly thinking of changing it to this behavior as well. It seems like SwiftUI basically only has one type of tap gesture.

2      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.