Search code examples
animationswiftuitransition

How to animate transition when adding view to hierarchy in SwiftUI


I am trying to build an overlay of "popover" views and animate transitioning in and out. Transitioning out works, but transitioning in doesn't -- as a popover view is added it just suddenly appears (wrong), but when a popover view is removed it slides out to the right (correct). How can I make the popover slide in (from right) when it's added to the view hierarchy in this code?

Fully functional code in iOS 14.

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        Popovers()
    }
}

struct Popovers : View {
    
    @State var popovers : [AnyView] = []
    
    var body : some View {
        Button("Add a view ...") {
            withAnimation {
                popovers += [new()]
            }
        }
        .blur(radius: 0 < popovers.count ? 8 : 0)
        .overlay(ZStack {
            ForEach(0..<self.popovers.count, id: \.self) { i in
                popovers[i]
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .blur(radius: (i+1) < popovers.count ? 8 : 0)
                    .transition(.move(edge: .trailing)) // works only when popover is removed
            }
        })
    }
    
    func new() -> AnyView {
        let popover = popovers.count
        
        return AnyView.init(
            VStack(spacing: 64) {
                
                Button("Close") {
                    withAnimation {
                        _ = popovers.removeLast()
                    }
                }
                .font(.largeTitle)
                .padding()

                Button("Add") {
                    withAnimation {
                        popovers += [new()]
                    }
                }
                .font(.largeTitle)
                .padding()

                Text("This is popover #\(popover)")
                    .font(.title)
                    .foregroundColor(.white)
                    .fixedSize()
                
            }
            .background(Color.init(hue: 0.65-(Double(3*popover)/100.0), saturation: 0.3, brightness: 0.9).opacity(0.98))
        )
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

extension View {
    var asAnyView : AnyView {
        AnyView(self)
    }
}

Solution

  • The solution is to add instead animation to container. Tested with Xcode 12 / iOS 14.

    demo

    struct Popovers : View {
        
        @State var popovers : [AnyView] = []
        
        var body : some View {
            Button("Add a view ...") {
                withAnimation {
                    popovers += [new()]
                }
            }
            .blur(radius: 0 < popovers.count ? 8 : 0)
            .overlay(ZStack {
                ForEach(0..<self.popovers.count, id: \.self) { i in
                    popovers[i]
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .blur(radius: (i+1) < popovers.count ? 8 : 0)
                        .transition(.move(edge: .trailing))
                }
            }.animation(.default))    // << add animation to container
        }
        
        func new() -> AnyView {
            let popover = popovers.count
            
            return AnyView.init(
                VStack(spacing: 64) {
                    
                    Button("Close") {
                                _ = popovers.removeLast()
                    }
                    .font(.largeTitle)
                    .padding()
    
                    Button("Add") {
                                popovers += [new()]
                    }
                    .font(.largeTitle)
                    .padding()
    
                    Text("This is popover #\(popover)")
                        .font(.title)
                        .foregroundColor(.white)
                        .fixedSize()
                    
                }
                .background(Color.init(hue: 0.65-(Double(3*popover)/100.0), saturation: 0.3, brightness: 0.9).opacity(0.98))
            )
        }
        
    }