Home > Net >  How to trigger SwiftUI animation via change of non-State variable
How to trigger SwiftUI animation via change of non-State variable

Time:02-11

It's easy to have an animation begin when a view appears, by using .onAppear(). But I'd like to perform a repeating animation whenever one of the view's non-State variables changes. An example:

Here is a view that I would like to "throb" whenever its throbbing parameter, set externally, is true:

struct MyCircle: View {
    var throbbing: Bool
    @State var scale = 1.0
    var body: some View {
        Circle()
            .frame(width: 100 * scale, height: 100 * scale)
            .foregroundColor(.blue)
            .animation(.easeInOut.repeatForever(), value: scale)
            .onAppear { scale = 1.2 }
    }
}

Currently the code begins throbbing immediately, regardless of the throbbing variable.

But imagine this scenario:

struct ContentView: View {
    @State var throb: Bool = false
    var body: some View {
        VStack {
            Button("Throb: \(throb ? "ON" : "OFF")") { throb.toggle() }
            MyCircle(throbbing: throb)
        }
    }
}

It looks like this:

Throbbing Circle

Any ideas how I can modify MyCircle so that the throbbing starts when the button is tapped and ends when it is tapped again?

CodePudding user response:

You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:

struct MyCircle: View {
    var throbbing: Bool
    @State private var scale : CGFloat = 0
    var body: some View {
        Circle()
            .frame(width: 100, height: 100)
            .scaleEffect(1   scale)
            .foregroundColor(.blue)
            .onChange(of: throbbing) { newValue in
                if newValue {
                    withAnimation(.easeInOut.repeatForever()) {
                        scale = 0.2
                    }
                } else {
                    withAnimation {
                        scale = 0
                    }
                }
            }
    }
}

CodePudding user response:

There're two interesting things that i've just found when trying to achieve yours target.

  • Animation will be added when the value changed
  • Animation that added to the view cannot be removed, and we need to drop the animated view from the view hiearachy by remove its id, new view will be created with zero animations.
import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    @State var throbbling = false
    @State var circleId = UUID()
    var body: some View {
        VStack {
            Toggle("Throbbling", isOn: $throbbling)
            circle.id(circleId)
                .scaleEffect(throbbling ? 1.2 : 1.0)
                .animation(.easeInOut.repeatForever(), value: throbbling)
                .onChange(of: throbbling) { newValue in
                    if newValue == false {
                        circleId = UUID()
                    }
                }
        }.padding()
    }
    
    @ViewBuilder
    var circle: some View {
        Circle().fill(.green).frame(width: 100, height: 100)
    }
}

PlaygroundPage.current.setLiveView(ContentView())
  • Related