Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animations triggered by events in SwiftUI

SwiftUI animations are typically driven by state, which is great, but sometimes you really want to trigger a temporary (often reversible) animation in response to some event. For example, I want to temporarily increase the size of a button when a it is tapped (both the increase and decrease in size should happen as a single animation when the button is released), but I haven't been able to figure this out.

It can sort of be hacked together with transitions I think, but not very nicely. Also, if I make an animation that uses autoreverse, it will increase the size, decrease it and then jump back to the increased state.

like image 245
Gusutafu Avatar asked Jul 29 '19 11:07

Gusutafu


People also ask

How do I know if animation is complete SwiftUI?

While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes. In other words, once the animation is done it will match our expected outcome. This is exactly how we know that an animation completes.

How do I make an animation in SwiftUI?

SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.

Does SwiftUI use Core Animation?

SwiftUI uses Core Animation for rendering by default, and its performance is great for most animations. But if you find yourself creating a very complex animation that seems to suffer from lower framerates, you may want to utilize the power of Metal, which is Apple's framework used for working directly with the GPU.


2 Answers

That is something I have been into as well.

So far my solution depends on applying GeometryEffect modifier and misusing the fact that its method effectValue is called continuously during some animation. So the desired effect is actually a transformation of interpolated values from 0..1 that has the main effect in 0.5 and no effect at 0 or 1

It works great, it is applicable to all views not just buttons, no need to depend on touch events or button styles, but still sort of seems to me as a hack.

Example with random rotation and scale effect:

enter image description here

Code sample:

struct ButtonEffect: GeometryEffect {

    var offset: Double // 0...1

    var animatableData: Double {
        get { offset }
        set { offset = newValue }
    }

    func effectValue(size: CGSize) -> ProjectionTransform {

        let effectValue = abs(sin(offset*Double.pi))
        let scaleFactor = 1+0.2*effectValue

        let affineTransform = CGAffineTransform(rotationAngle: CGFloat(effectValue)).translatedBy(x: -size.width/2, y: -size.height/2).scaledBy(x: CGFloat(scaleFactor), y: CGFloat(scaleFactor))

        return ProjectionTransform(affineTransform)
    }
}

struct ButtonActionView: View {
    @State var animOffset: Double = 0
    var body: some View {
        Button(action:{
            withAnimation(.spring()) {
                self.animOffset += 1
            }
        })
        {
            Text("Press ME")
                .padding()
        }
        .background(Color.yellow)
        .modifier(ButtonEffect(offset: animOffset))
    }
}
like image 85
Pavel Zak Avatar answered Oct 11 '22 08:10

Pavel Zak


You can use a @State variable tied to a longPressAction():

enter image description here

Code updated for Beta 5:

struct ContentView: View {
    var body: some View {
        HStack {
            Spacer()
            MyButton(label: "Button 1")
            Spacer()
            MyButton(label: "Button 2")
            Spacer()
            MyButton(label: "Button 3")
            Spacer()
        }
    }
}

struct MyButton: View {
    let label: String
    @State private var pressed = false


    var body: some View {

        return Text(label)
            .font(.title)
            .foregroundColor(.white)
            .padding(10)
            .background(RoundedRectangle(cornerRadius: 10).foregroundColor(.green))
            .scaleEffect(self.pressed ? 1.2 : 1.0)
            .onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
                withAnimation(.easeInOut(duration: 0.2)) {
                    self.pressed = pressing
                }
            }, perform: { })
    }
}
like image 29
kontiki Avatar answered Oct 11 '22 10:10

kontiki