I am working on a SwiftUI project. I want to animate the switching between the child views.
For now I have applied the animation inside a child view with onAppear modifier.
I basically have 3 images that repeats on loop forever. But the transition between those images is not happening. I mean it works only for the 1st image and the rest of the 2 images seem to have a solid/no-transition at all.
File 1:
import SwiftUI
struct ContentView: View {
@State private var currentIndex = 1
@State private var isAnimating: Bool = false
let profiles: [Profile] = [.image1, .image2, .image3]
var body: some View {
ScrollView {
ZStack {
MainScreenProfile(profile: profiles[currentIndex])
.transition(.move(edge: .trailing))
.onAppear {
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
self.currentIndex = (self.currentIndex + 1) % self.profiles.count
}
}
Spacer()
//Play stranger things theme song
//AudioPlayer()
.padding()
} //VSTACK
VStack(spacing: 20) {
ForEach(0 ..< 20) { _ in
CastView()
}
}
} //SCROLLVIEW
.background(Color.black) // Set background color to ensure the gradient mask works
}
}
#Preview {
ContentView()
}
File 2:
import SwiftUI
enum Profile: String {
case image1
case image2
case image3
}
struct MainScreenProfile: View {
let profile: Profile
@State private var startAnimation: Bool = false
@State var isAnimating: Bool = false
var body: some View {
VStack {
Image(profile.rawValue)
.resizable()
.scaledToFill()
.frame(height: 700) // Adjust the height as needed
.mask(LinearGradient(gradient: Gradient(stops: [
.init(color: .black, location: 0),
.init(color: .clear, location: 1), // Adjust the location as needed
.init(color: .black, location: 1),
.init(color: .clear, location: 1)
]), startPoint: .top, endPoint: .bottom))
.opacity(startAnimation ? 1.0 : 0.0)
.onAppear {
withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
startAnimation.toggle()
}
}
} //: VSRACK
}
}
#Preview {
MainScreenProfile(profile: Profile.image1)
}
Also tried using the .transition modifier like this:
MainScreenProfile(profile: profiles[currentIndex]) .transition(.move(edge: .bottom))
But this seems to not work.
The reason why no animation is happening is because .onAppear
is only called when the view is shown for the first time. After that, it is already visible, so .onAppear
is not called again, even though the parameters to the view may have changed.
To fix, try using .onChange
to change the flag. You probably want to reset to false and then change to true with animation, otherwise every second image is seen to get darker instead of getting brighter.
If you are running iOS 17 then .onAppear
is no longer needed. To make the transition a bit smoother, you could also use a completion callback to reset the flag:
// MainScreenProfile
Image(profile.rawValue)
// modifiers as before
// .onAppear {
// withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
// startAnimation.toggle()
// }
// }
.onChange(of: profile, initial: true) {
withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
startAnimation = true
} completion: {
withAnimation(.easeInOut.delay(1.2)) {
startAnimation = false
}
}
}
If you are running pre-iOS 17, you need to use a different .onChange
call and also keep the .onAppear
for the initial show:
Image(profile.rawValue)
// modifiers as before
.onAppear {
withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
startAnimation.toggle()
}
}
.onChange(of: profile) { newVal in
startAnimation = false
withAnimation(Animation.easeInOut(duration: 1).delay(0.5)) {
startAnimation = true
}
}