Search code examples
swiftuiswiftui-animation

Can you defer evaluation of a variable used in a view removal transition?


I've got a parent view that presents one of a couple of views, depending on the value of an observed variable.

In the simple example below, RainbowView is presented when the @Published variable "settings.showRainbow" is true.

The RainbowView sets "settings.showRainbow" to false when the user taps one of the colours of the rainbow, which causes the parent view to remove it, and display another view. The tap location is also set by RainbowView.

When RainbowView is removed, its removal transition animation is to zoom in around the location where the user tapped.

It works perfectly ... except, of course, that the value of the removal transition's .scale anchor, the variable "tapLocation", is set when the view is presented, rather than when it is removed.

What I'd like, is for SwiftUI to wait to capture the anchor location until the point in time where the view is about to be removed.

Does anyone know if this is possible? I could make the child view responsible for animating itself when it's about to be dismissed (which works OK), but I would prefer to have the parent view be responsible for the removal animation.

Here's a code snippet:

if settings.showRainbow {
   RainbowView()
   .transition(AnyTransition
        .asymmetric(
        insertion: .opacity,
        removal: .scale(scale: 24,
                        anchor: tapLocation)))
} else {
     OtherView()

Solution

  • It seems to work if the flag showRainbow is reset asynchronously, after setting the tap location.

    I found that it was necessary to perform the reset after a tiny delay: 0.01s was not always reliable, 0.05s works better.

    struct RainbowView: View {
        let rainbowColors: [Color] = [.red, .orange, .yellow, .green, .blue, .indigo, .purple]
        var body: some View {
            ZStack {
                ForEach(Array(rainbowColors.enumerated()), id: \.offset) { offset, color in
                    Circle()
                        .trim(from: 0.5, to: 1.0)
                        .stroke(color, lineWidth: 10)
                        .padding(CGFloat(offset) * 10)
                }
            }
        }
    }
    
    struct ContentView: View {
        let rainbowSize: CGFloat = 250
        @State private var showRainbow = false
        @State private var tapLocation = UnitPoint.zero
    
        var body: some View {
            ZStack {
    //            if settings.showRainbow {
                if showRainbow {
                    RainbowView()
                        .frame(width: rainbowSize)
                        .transition(
                            .asymmetric(
                                insertion: .opacity,
                                removal: .scale(scale: 24, anchor: tapLocation)
                            )
                        )
                        .onTapGesture { location in
                            tapLocation = UnitPoint(
                                x: location.x / rainbowSize,
                                y: location.y / rainbowSize
                            )
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
                                showRainbow = false
                            }
                        }
                } else {
                    Text("OtherView")
                        .onTapGesture { showRainbow = true }
                }
            }
            .animation(.easeInOut(duration: 1), value: showRainbow)
        }
    }
    

    Animation