Today I faced an issue with animations depending on which kind of ForEach I use. To be honest I don’t understand what exactly the issue is.
I created this little example for you.
The AudioIndicators View contains 3 different examples of animated bars. Only the last one (green) is animating fine. The only difference is the kind of ForEach I used.
Can someone explain why the animation is not working for the blue and red bars?
import SwiftUI
struct BarView: View {
var value: CGFloat
var tint: Color = .accentColor
var barHeight: CGFloat = 100
var body: some View {
RoundedRectangle(cornerRadius: 3)
.fill(tint)
.frame(width: .infinity, height: barHeight * value)
.animation(.linear(duration: 0.5))
}
}
struct AudioIndicators: View {
@Binding var samples: [CGFloat]
let barSpacing: CGFloat = 4
var body: some View {
VStack(alignment: .center) {
HStack(spacing: 32) {
// BLUE
HStack(alignment: .center, spacing: barSpacing) {
ForEach(samples, id: \.self) { sample in
BarView(value: sample, tint: .blue)
}
}
// RED
HStack(alignment: .center, spacing: barSpacing) {
ForEach(Array(samples.enumerated()), id: \.element) { index, sample in
BarView(value: sample, tint: Color.red)
}
}
// GREEN
HStack(alignment: .center, spacing: barSpacing) {
ForEach(0 ..< samples.count, id: \.self) { i in
BarView(value: samples[i], tint: Color.green)
}
}
}
}
}
}
struct ContentView: View {
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
@State var samples: [CGFloat] = [0, 0, 0, 0, 0]
var body: some View {
AudioIndicators(samples: $samples)
.onReceive(timer) { input in
samples = randomSamples()
}
}
func randomSamples() -> [CGFloat] {
[randomSample(), randomSample(), randomSample(), randomSample(), randomSample()]
}
func randomSample() -> CGFloat {
CGFloat.random(in: 0 ... 2)
}
}
#Preview {
ContentView()
}
The identities of views play an important part in how they are animated.
If the identity of a view has changed, that means (as far as SwiftUI is concerned) this is a different view. If the view update is animated, SwiftUI will treat these as separate views - the old view disappears, and a new view appears.
If the view's identity doesn't change, it's still the same view. If there are other changes to the view, and the view update is animated, SwiftUI will animate the other changes to the view.
In your code, the
BarViews created by the first twoForEaches all change identities whensamplesis updated. Namely, their identities are theCGFloats in the sample. For example, ifsamplesis[0.1, 0.2, 0.3], then the firstBarViewhas id0.1, the second has0.2, and the thirdBarViewhas id0.3.Suppose
sampleschanges to[0.4, 0.5, 0.6]. All the existingBarViews change identity, so they disappear, and newBarViews with new identities are created. This is not animated, but you can animate it by adding:in
AudioIndicators. You will see that the bars fade in and fade out.In the third
ForEach, the views' identities don't change whensampleschanges. The first bar always has identity0, the second bar always has identity1, etc. Therefore, whensampleschange, SwiftUI can understand that this is the same bar, just with a different height. And because you added theanimationmodifier to the rectangle, it animates the change in the bar's height.For more info about identities, see Demystifying SwiftUI.
Side notes:
width: .infinity. Dimensions like this should not be negative or infinite. You might have meantmaxWidth: .infinity(which would need to go in a differentframemodifier), but shapes likeRoundedRectangle, naturally expand to fill the available space, so there is no need to do that.animationmodifier is deprecated. You should add avalue:argument to it, to indicate which property you want to animate. e.g..animation(.linear(duration: 0.5), value: value)