SwiftUI Rerun animation when property changes.

I have a background view that shows a status indicator behind the main content. I have it set to animate and I would like it to animate when the status changes, I'm passing the status to the background view. However, it's only animating the first time it appears rather than when the status is changing.
I'm using onAppear to turn the animation on, but it seems onAppear is only called the first time the view is rendered rather than when the content changes.

I've tried without success:
Using onChange(of) with the status.
Bringing the animation logic into the primary view.

I do have a workaround in my repro code (commented), but there must be a better approach.

xcode 12.4/iOS 14.4 & xcode 12.5/iOS 14.5

Code Block
struct ContentView: View {
@State private var status: String = "1"
var body: some View {
VStack(spacing: 0) {
Spacer()
BackgroundView(status: status)
// This is my current workaround, but looking for a better way
// if status != "1" && status != "3" {
// BackgroundView(status: status)
// } else if status == "3" {
// BackgroundView(status: status)
// } else {
// BackgroundView(status: status)
// }
Spacer()
Button(action: {
if status == "1" {
status = "2"
} else if status == "2" {
status = "3"
} else {
status = "1"
}
}, label: {
Text("next status")
})
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct BackgroundView: View {
var status: String = "1"
@State private var isAnimating: Bool = false
var body: some View {
ZStack {
Group {
Image(systemName: "\(status).circle.fill")
.resizable()
.scaledToFit()
.frame(width: 220)
.foregroundColor(.red)
}
.scaleEffect(isAnimating ? 1.0 : 0, anchor: .top)
}
.animation(Animation.easeOut(duration: 0.4), value: isAnimating)
.onAppear(perform: {
print("appear: \(status)")
isAnimating = true
})
// .onDisappear(perform: {
// print("disappear: \(status)")
// isAnimating = false // doesn't help
// } )
}
}

Answered by OOPer in 671584022
How about something like this?
Code Block
struct ContentView: View {
@State private var status: String = ""
var body: some View {
VStack(spacing: 0) {
Spacer()
BackgroundView(status: $status)
Spacer()
Button(action: {
if status == "1" {
status = "2"
} else if status == "2" {
status = "3"
} else {
status = "1"
}
}, label: {
Text("next status")
})
Spacer()
}
.onAppear {
status = "1"
}
}
}
struct BackgroundView: View {
@Binding var status: String
@State var isAnimating: Bool = false
var body: some View {
ZStack {
Group {
Image(systemName: "\(status).circle.fill")
.resizable()
.scaledToFit()
.frame(width: 220)
.foregroundColor(.red)
}
.scaleEffect(isAnimating ? 1.0 : 0, anchor: .top)
}
.onChange(of: status) {_ in
isAnimating = false
withAnimation(Animation.easeOut(duration: 0.4)) {
isAnimating = true
}
}
}
}

Your BackgroundView will animate as expected when isAnimating changes from false to true.
So, do it onChange of status.
Accepted Answer
How about something like this?
Code Block
struct ContentView: View {
@State private var status: String = ""
var body: some View {
VStack(spacing: 0) {
Spacer()
BackgroundView(status: $status)
Spacer()
Button(action: {
if status == "1" {
status = "2"
} else if status == "2" {
status = "3"
} else {
status = "1"
}
}, label: {
Text("next status")
})
Spacer()
}
.onAppear {
status = "1"
}
}
}
struct BackgroundView: View {
@Binding var status: String
@State var isAnimating: Bool = false
var body: some View {
ZStack {
Group {
Image(systemName: "\(status).circle.fill")
.resizable()
.scaledToFit()
.frame(width: 220)
.foregroundColor(.red)
}
.scaleEffect(isAnimating ? 1.0 : 0, anchor: .top)
}
.onChange(of: status) {_ in
isAnimating = false
withAnimation(Animation.easeOut(duration: 0.4)) {
isAnimating = true
}
}
}
}

Your BackgroundView will animate as expected when isAnimating changes from false to true.
So, do it onChange of status.
Thanks OOPer!

Turns out all I needed was to add the onChange you suggested - needing both the onAppear and onChange. So the BackgroundView looks like this:

Code Block swift
struct BackgroundView: View {
var status: String = "0"
@State private var isAnimating: Bool = false
var body: some View {
ZStack {
Group {
Image(systemName: "\(status).circle.fill")
.resizable()
.scaledToFit()
.frame(width: 220)
.foregroundColor(.red)
}
.scaleEffect(isAnimating ? 1.0 : 0, anchor: .top)
}
.animation(Animation.easeOut(duration: 0.4), value: isAnimating)
.onAppear(perform: {
print("appear: \(status)")
isAnimating = true
})
.onChange(of: status) { _ in
print("change: \(status)")
isAnimating = false
withAnimation(Animation.easeOut(duration: 0.4)) {
isAnimating = true
}
}
}
}


SwiftUI Rerun animation when property changes.
 
 
Q