LN-12
LN-12

Reputation: 1039

SwiftUI - update target view during a transition

I want to transition between two views in SwiftUI using a horizontal sliding transition. The problem is, that I also want to update the target view once the data is fetched from the network.

Down below is a minimal example of the transition. When the button on the first view is pressed, the transition and the (placeholder) background work is started. For better visibility, the transition is slowed down. In the second view, I have a ProgressView which should be replaced with the actual view (here a Text view) once the data is available.

@main
struct MyApp: App {
    @StateObject private var viewModel = ViewModel()
    @State private var push = false
    private let transition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
                                                      removal: .move(edge: .leading))
    private let transitionAnimation = Animation.easeOut(duration: 3)

    var body: some Scene {
        WindowGroup {
            if !push {
                VStack(alignment: .center) {
                    HStack { Spacer() }
                    Spacer()

                    Button(action: {
                        push.toggle()
                        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
                            DispatchQueue.main.sync {
                                self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
                            }
                        }
                    }){
                        Text("Go")
                    }

                    Spacer()
                }
                .background(Color.green)
                .transition(transition)
                .animation(transitionAnimation)
            } else {
                SecondView()
                    .transition(transition)
                    .animation(transitionAnimation)
                    .environmentObject(viewModel)
            }
        }
    }
}

final class ViewModel: NSObject, ObservableObject {
    @Published var someText: String = ""
}

struct SecondView: View {
    @EnvironmentObject var viewModel: ViewModel

    var body: some View {
        VStack(alignment: .center) {
            HStack { Spacer() }
            Spacer()

            if(viewModel.someText.isEmpty) {
                ProgressView()
                    .progressViewStyle(CircularProgressViewStyle())
            } else {
                Text(viewModel.someText)
            }

            Spacer()
        }.background(Color.red)
    }
}

The problem now is that the Text view is not included in the view transition. I would expect that it is moving along with the transition (= the red area), but instead, it just appears at the location where it would be after the transition. The following animation shows this effect.

enter image description here

Is it possible to achieve the animation of the Text view? To be clear: I know that in this case I could just always display the Text view because the string is empty at the beginning. As I stated earlier, this is a massively simplified version of my actual view hierarchy. I don't see a way of leaving out the if-else statement or use the hidden modifier.

Upvotes: 3

Views: 1235

Answers (1)

Egor Komarov
Egor Komarov

Reputation: 523

Few things have to be done to make it work properly.

First, Text should exist in the views hierarchy even when the someText is empty. You can wrap it and the progress indicator into ZStack and control the text visibility with .opacity instead of the if/else statement.

Second, the animations can be applied conditionally depending on which value has changed. You should apply transitionAnimation only when the push variable is changed:

.animation(transitionAnimation, value: push)

There is a catch though: it won't work from the if/else branches on the same variable, because each of the .animation statements will exist only for one value of the push variable, and the changes in it won't be noticed. To fix that if/else should be wrapped into a Group, and animation should be applied to it.

Here is a full solution:

@main
struct MyApp: App {
    @StateObject private var viewModel = ViewModel()
    @State private var push = false
    private let transition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
                                                      removal: .move(edge: .leading))
    private let transitionAnimation = Animation.easeOut(duration: 3)

    var body: some Scene {
        WindowGroup {
            Group {
                if !push {
                    VStack(alignment: .center) {
                        HStack { Spacer() }
                        Spacer()

                        Button(action: {
                            push.toggle()
                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                                self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
                            }
                        }){
                            Text("Go")
                        }

                        Spacer()
                    }
                    .background(Color.green)
                    .transition(transition)
                } else {
                    SecondView()
                        .transition(transition)
                        .environmentObject(viewModel)
                }
            }
            .animation(transitionAnimation, value: push)
        }
    }
}

final class ViewModel: NSObject, ObservableObject {
    @Published var someText: String = ""
}

struct SecondView: View {
    @EnvironmentObject var viewModel: ViewModel

    var body: some View {
        VStack(alignment: .center) {
            HStack { Spacer() }
            Spacer()

            ZStack {
                if(viewModel.someText.isEmpty) {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle())
                }
                Text(viewModel.someText)
                    .opacity(viewModel.someText.isEmpty ? 0.0 : 1.0)
            }

            Spacer()
        }.background(Color.red)
    }
}

Upvotes: 2

Related Questions