Why isn't my SwiftUI transition animating?

1.4k views Asked by At

I have a view based on a few state variables. These states are set by the completion block of an API call. The bookings are displayed as a ForEach list and I want them to appear with the slide transition.

The slide transition worked perfectly. Since I added the !isLoading check, these animations no longer display. All the animations that appear now are the fade in/out of the ProgressView. When removing the !isLoading check, the staggered slide animation comes back, but I want to keep this check so I can properly handle loading states.

Here is a standalone code to reproduce the issue:

import SwiftUI

struct RoomView: View {
    
    @State private var room: String?
    @State private var bookings: [String]?
    @State private var isLoading: Bool = false
    
    var body: some View {
        VStack(spacing: 0) {
            if /* This is the problematic condition !isLoading, */ let room = self.room {
                ScrollView {
                    LazyVStack(alignment: .leading) {
                        Text(room)
                        
                        if let bookings = bookings {
                            ForEach(bookings.indices) { (index) in
                                let booking = bookings[index]
                                VStack(alignment: .leading) {
                                    Text(booking)
                                    Divider()
                                }
                                .transition(.slide)
                                // Stagger the animations when displaying the list
                                .animation(Animation.spring().delay(0.04 * Double(index)))
                            }
                        }
                    }
                    .padding()
                }
            } else {
                // If we're loading
                ProgressView("Loading Room")
            }
        }
        .transition(.opacity)
        .animation(.easeInOut)
        .onAppear {
            self.isLoading = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.room = "Demo room"
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    withAnimation {
                        self.bookings = ["Booking 1", "Booking 2", "Booking 3", "Booking 4", "Booking 5"]
                        self.isLoading = false
                    }
                }
            }
        }
    }
}

struct RoomView_Previews: PreviewProvider {
    static var previews: some View {
        RoomView()
    }
}

Is there a known issue with SwiftUI animations where having multiple conditions could cause transitions to not appear? How can I fix this so I keep my staggered animation & fade of the ProgressView transitions without removing the !isLoading check?

1

There are 1 answers

1
Asperi On

Here is possible solution - make views independent. Tested with Xcode 12 / iOS 14.

demo

struct RoomView: View {
    
    @State private var room: String?
    @State private var bookings: [String]?
    @State private var isLoading: Bool = false
    
    var body: some View {
        ZStack {
            if isLoading {
                // If we're loading
                ProgressView("Loading Room")
            }
            if let room = self.room {
                ScrollView {
                    LazyVStack(alignment: .leading) {
                        Text(room)
                        
                        if let bookings = bookings {
                            ForEach(bookings.indices) { (index) in
                                let booking = bookings[index]
                                VStack(alignment: .leading) {
                                    Text(booking)
                                    Divider()
                                }
                                .transition(.slide)
                                // Stagger the animations when displaying the list
                                .animation(Animation.spring().delay(0.04 * Double(index)))
                            }
                        }
                    }
                    .padding()
                }
            }
        }
        .transition(.opacity)
        .animation(.easeInOut)
        .onAppear {
            self.isLoading = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.room = "Demo room"
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    withAnimation {
                        self.bookings = ["Booking 1", "Booking 2", "Booking 3", "Booking 4", "Booking 5"]
                        self.isLoading = false
                    }
                }
            }
        }
    }
}