Animates wrongly at every switch of direction, animates right in same direction

If you run the app and keep pressing next, it works well.

current question moves out towards left, next question appears from right to left.

But if I switch the direction to previous,

current question moves out towards left( should move out towards right) next question comes in from left to right( which is correct).

Similar discrepancy happens when I switch direction from prev to next.

Why is this so?


import SwiftUI
 
struct Question {
    let id: Int
    let text: String
}
extension AnyTransition {
    static var slideRight: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
        let removal = AnyTransition.move(edge: .leading)
        return .asymmetric(insertion: insertion, removal: removal)
    }
    
    static var slideLeft: AnyTransition {
        let insertion = AnyTransition.move(edge: .leading)
        let removal = AnyTransition.move(edge: .trailing)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}
struct QuizView: View {
    let questions = [
        Question(id: 1, text: "11111111111"),
        Question(id: 2, text: "222222222222222222222222"),
        Question(id: 3, text: "3333333333333333333"),
        Question(id: 4, text: "444444444444444444444444"),
        Question(id: 5, text: "55555555555555555555"),
        Question(id: 6, text: "6666666666666666666666666")
    ]
    
    @State private var currentQuestionIndex = 0
    @State private var navigationDirection: NavigationDirection = .forward
    
    var body: some View {
        VStack(spacing: 20) {
            Text(questions[currentQuestionIndex].text)
                .id(questions[currentQuestionIndex].id) // Important for transition
                .transition(navigationDirection == .forward ? .slideRight : .slideLeft)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            HStack {
                Button("Previous") {
                    moveToPreviousQuestion()
                }
                .disabled(currentQuestionIndex == 0)
                
                Spacer()
                
                Button("Next") {
                    moveToNextQuestion()
                }
                .disabled(currentQuestionIndex == questions.count - 1)
            }
        }
        .padding()
        .animation(.easeInOut(duration: 1.0), value: currentQuestionIndex)

    }
    
    private func moveToNextQuestion() {
        if currentQuestionIndex < questions.count - 1 {
                navigationDirection = .forward
                currentQuestionIndex += 1
        }
    }
    
    private func moveToPreviousQuestion() {
        if currentQuestionIndex > 0 {
                navigationDirection = .backward
                currentQuestionIndex -= 1
        }
    }
}

enum NavigationDirection {
    case forward, backward
}

Replies

SwiftUI remembers the "old" view in order to perform a transition to the "new" view. When you reverse direction, the outgoing view is still associated with a transition going in the (now) wrong direction. My fix is to immediately change the direction when you press the button. This causes body() to be re-invoked, but no transition is visible because the view ID of the question has not changed. Subsequently, a Task alters the view ID, which causes a transition from the old question (now associated with a removal transition in the new direction) to the new question.

I think this solution smells a little, because it is imperatively driving the UI, and because I have no idea when the Task closures will actually be invoked. No doubt after the Button's action closure, but are they guaranteed to be invoked before any subsequent UI action? I don't know.

I changed QuizView to ContentView to get it to compile in my test app.

import SwiftUI
 
struct Question {
    let id: Int
    let text: String
}
extension AnyTransition {
    static var slideRight: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
        let removal = AnyTransition.move(edge: .leading)
        return .asymmetric(insertion: insertion, removal: removal)
    }
    
    static var slideLeft: AnyTransition {
        let insertion = AnyTransition.move(edge: .leading)
        let removal = AnyTransition.move(edge: .trailing)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}
struct ContentView: View {
    let questions = [
        Question(id: 1, text: "11111111111"),
        Question(id: 2, text: "222222222222222222222222"),
        Question(id: 3, text: "3333333333333333333"),
        Question(id: 4, text: "444444444444444444444444"),
        Question(id: 5, text: "55555555555555555555"),
        Question(id: 6, text: "6666666666666666666666666")
    ]
    
    @State private var currentQuestionIndex = 0
    @State private var navigationDirection: NavigationDirection = .forward
    
    var body: some View {
        VStack(spacing: 20) {
            Text(questions[currentQuestionIndex].text)
                .id(questions[currentQuestionIndex].id) // Important for transition
                .transition(navigationDirection == .forward ? .slideRight :  .slideLeft)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            HStack {
                Button("Previous") {
                    moveToPreviousQuestion()
                }
                .disabled(currentQuestionIndex == 0)
                
                Spacer()
                
                Button("Next") {
                    moveToNextQuestion()
                }
                .disabled(currentQuestionIndex == questions.count - 1)
            }
        }
        .padding()
        .animation(.easeInOut(duration: 1.0), value: currentQuestionIndex)

    }
    
    private func moveToNextQuestion() {
        if currentQuestionIndex < questions.count - 1 {
            if navigationDirection == .backward {
                navigationDirection = .forward
            }
            Task {
                currentQuestionIndex += 1
            }
        }
    }
    
    private func moveToPreviousQuestion() {
        if currentQuestionIndex > 0 {
            if navigationDirection == .forward {
                navigationDirection = .backward
            }
            Task {
                currentQuestionIndex -= 1
            }
        }
    }
}

enum NavigationDirection {
    case forward, backward
}