Search code examples
iosswiftuitransitionswiftui-transition

How to Animate between views in SwiftUI with a transition effect


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.


Solution

  • 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
            }
        }