Search code examples
animationswiftuiopacityzstackeaseinout

SwiftUI - animating View opacity in ZStack with .easeInOut


I have a view sitting on top of a mapView (in a ZStack) and want to be able to have the green, upper view fade in and out with the .easeInOut animation modifier applied to the view's opacity. As you can see in the gif, it fades in nicely but disappears abruptly.

enter image description here

If I remove the mapView then all is good. If I replace the mapView with a simple Rectangle() then the problem returns so I believe it has something to do with the ZStack rather than the map. Funnily enough I'm actually using Mapbox rather than MapKit (as in the code below for simplicity) and the fade/abrupt disappear behaviour is reversed.

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var show = false

    var body: some View {
        VStack {
            ZStack {
                MapView()
                if show {
                    LabelView()
                        .transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
                }
            }
            Button("Animate") {
                self.show.toggle()
            }.padding(20)
        }
    }
}

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.mapType = .standard
        return mapView
    }

    func updateUIView(_ uiView: MKMapView, context: Context) { }
}

struct LabelView: View {
    var body: some View {
        Text("Hi there!")
            .padding(10)
            .font(.title)
            .foregroundColor(.white)
            .background(RoundedRectangle(cornerRadius: 8).fill(Color.green).shadow(color: .gray, radius: 3))
    }
}

I have tried using alternative animation code, replacing the LabelView transition with:

.transition(.opacity)

and changing the button code to:

Button("Animate") {
    withAnimation(.easeInOut(duration: 1.0)) {
        self.show.toggle()
    }
}

but the same behaviour appears each time. I'm guessing this is a SwiftUI bug but couldn't find any previous reference.


Solution

  • Here is working solution. Tested with Xcode 11.4 / iOS 13.4.

    demo

    As it seen in demo presence of transparent label does not affect functionality of map view.

        var body: some View {
            VStack {
                ZStack {
                    MapView()
                    LabelView().opacity(show ? 1 : 0)   // here !!
                }.animation(.easeInOut(duration: 1.0))
    
                Button("Animate") {
                    self.show.toggle()
                }.padding(20)
            }
        }
    

    Another alternate, actually with the same visual effect is to embed LabelView into container and apply transition to it (it must be left something to render view disappearing)

    var body: some View {
        VStack {
            ZStack {
                MapView()
                VStack {              // << here !!
                    if show {
                        LabelView()
                    }
                }.transition(.opacity).animation(.easeInOut(duration: 1.0))
            }
            Button("Animate") {
                self.show.toggle()
            }.padding(20)
        }
    }