Search code examples
iosxcodeanimationswiftuitransition

SwiftUI Can't Animate Image Transitions


I'm trying to make a simple image selector and I would like the transition from one image to another to be animated. Seems like a really simple thing to do, but I have not been able to find a single solution. I must be missing something really simple here.

Here's a simplified version:

struct ContentView: View {
    let photos = ["gear", "person", "person.2", "car", "leaf"]

    @State private var currentIndex: Int = 0
    @State private var showAnimation: Bool = true
    private var scalingFactor: CGFloat = 0.5

    var body: some View {

        VStack {
            Text("Image Detection")
                .font(.system(size: 30))
                .fontWeight(.heavy)
                .padding(.top, 30)
        
            Spacer()

            Image(systemName: photos[currentIndex])
                .resizable()
                .frame(width: 250, height: 250)
            //this does nothing
                //.transition(.move(edge: .bottom))
            //this does nothing
                //.animation(.spring(), value: currentIndex)
            //this does nothing
                //.animation(.easeInOut, value: showAnimation)
            //this does nothing
                //.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
            //this does nothing
//                .animation(showAnimation ? Animation.easeOut(duration: 2.0) : Animation.easeOut(duration: 1.0), value: showAnimation)

            //this works - but alternates scaled and not with each image
            //and is not really an animation
                .scaleEffect(showAnimation ? 1.0 : 0.5)

            Spacer()
        
            HStack {
                Button(action: {
                    if self.currentIndex >= self.photos.count {
                        self.currentIndex = self.currentIndex - 1
                    } else {
                        self.currentIndex = 0
                    }
                    withAnimation {
                        self.showAnimation.toggle()
                    }
                }, label: {
                    Image(systemName: "arrowtriangle.backward.fill")
                })
                    .padding()
                    .foregroundColor(Color.blue)
                    .font(.largeTitle)

                Spacer()
            
                Button(action: {
                    if self.currentIndex < self.photos.count - 1 {
                        self.currentIndex = self.currentIndex + 1
                    } else {
                        self.currentIndex = self.photos.count - 1
                    }
                    withAnimation {
                        self.showAnimation.toggle()
                    }
                }, label: {
                    Image(systemName: "arrowtriangle.forward.fill")
                })
                    .padding()
                    .foregroundColor(Color.blue)
                    .font(.largeTitle)

            }
            .padding(.horizontal, 50)

            Spacer()
        }//v
    }//body
}//content view

Any guidance would be appreciated. Xcode 13.2.1 iOS 15.2


Solution

  • The current approach with SwiftUI is to use .transition(), because .animation() is being deprecated.

    What is important to understand is that .transition() is triggered when a view appears or disappears. Your view will not be completely re-drawn just because you change a @State variable: in your code, the Image changes but it always stays in the view.

    One solution is to trigger the image to completely disappear and make a new one re-appear. The code below does that, depending on the state of showAnimation. See that I only used .transition(), but for a nice effect:

    • it is asymmetric
    • the withAnimation() closure wraps also the changing of the currentIndex
    struct Example: View {
        let photos = ["gear", "person", "person.2", "car", "leaf"]
    
        @State private var currentIndex: Int = 0
        @State private var showAnimation: Bool = true
        private var scalingFactor: CGFloat = 0.5
    
        var body: some View {
    
            VStack {
                Text("Image Detection")
                    .font(.system(size: 30))
                    .fontWeight(.heavy)
                    .padding(.top, 30)
            
                Spacer()
                
                if showAnimation {
                    image
                } else {
                    image
                }
    
                Spacer()
            
                HStack {
                    Button(action: {
                        withAnimation {
                            showAnimation.toggle()
                            if self.currentIndex >= self.photos.count {
                                self.currentIndex = self.currentIndex - 1
                            } else {
                                self.currentIndex = 0
                            }
                        }
                    }, label: {
                        Image(systemName: "arrowtriangle.backward.fill")
                    })
                        .padding()
                        .foregroundColor(Color.blue)
                        .font(.largeTitle)
    
                    Spacer()
                
                    Button(action: {
                        withAnimation {
                            showAnimation.toggle()
                            if self.currentIndex < self.photos.count - 1 {
                                self.currentIndex = self.currentIndex + 1
                            } else {
                                self.currentIndex = self.photos.count - 1
                            }
                        }
                    }, label: {
                        Image(systemName: "arrowtriangle.forward.fill")
                    })
                        .padding()
                        .foregroundColor(Color.blue)
                        .font(.largeTitle)
    
                }
                .padding(.horizontal, 50)
    
                Spacer()
            }//v
        }//body
        
        private var image: some View {
            Image(systemName: photos[currentIndex])
                .resizable()
                .frame(width: 250, height: 250)
                .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
        }
    }