2

I'm having trouble creating transitions that properly emulate iOS navigation transitions. Different transitions should play depending on whether the user is navigating "forward" or "backward", but it seems like the view inside my switch statement isn't getting the most recent version of the state variable being updated. This results in strange behavior - after "going" forward, backward, and forward again, the view that's being removed isn't reading the newest value of my boolean variable and plays the old transition instead.

I understand why the problem is occurring, but it's extremely frustrating as I've spent the last two hours trying to fix this issue with no success. I've attached code and a GIF below. Thank you!

ContentView.swift

import SwiftUI

enum NavigationViewType {
    case firstView, secondView, thirdView
}

struct ContentView: View {
    @State private var currentView: NavigationViewType = .firstView
    @State private var isReverse = false
    
    var body: some View {
        VStack {
            switch currentView {
            case .firstView:
                FirstView(nextView: goToNextView, goBack: goBack, isReverse: $isReverse)
                    .transition(isReverse ? .reverseSlide : .slide)
            case .secondView:
                SecondView(nextView: goToNextView, goBack: goBack, isReverse: $isReverse)
                    .transition(isReverse ? .reverseSlide : .slide)
            case .thirdView:
                ThirdView(nextView: goToNextView, goBack: goBack, isReverse: $isReverse)
                    .transition(isReverse ? .reverseSlide : .slide)
            }
        }
        .animation(.default)
    }
    
    func goToNextView() {
        withAnimation {
            isReverse = false
            switch currentView {
            case .firstView:
                currentView = .secondView
            case .secondView:
                currentView = .thirdView
            case .thirdView:
                currentView = .firstView
            }
        }
    }
    
    func goBack() {
        withAnimation {
            isReverse = true
            switch currentView {
            case .firstView:
                currentView = .thirdView
            case .secondView:
                currentView = .firstView
            case .thirdView:
                currentView = .secondView
            }
        }
    }
}

struct FirstView: View {
    var nextView: () -> Void
    var goBack: () -> Void
    @Binding var isReverse: Bool
    
    var body: some View {
        VStack {
            Text("First View")
            Button("Next") {
                nextView()
            }
            Button("Back") {
                isReverse = true
                goBack()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue)
    }
}

struct SecondView: View {
    var nextView: () -> Void
    var goBack: () -> Void
    @Binding var isReverse: Bool
    
    var body: some View {
        VStack {
            Text("Second View")
            Button("Next") {
                nextView()
            }
            Button("Back") {
                isReverse = true
                goBack()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.green)
    }
}

struct ThirdView: View {
    var nextView: () -> Void
    var goBack: () -> Void
    @Binding var isReverse: Bool
    
    var body: some View {
        VStack {
            Text("Third View")
            Button("Next") {
                nextView()
            }
            Button("Back") {
                isReverse = true
                goBack()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.yellow)
    }
}

extension AnyTransition {
    static var slide: AnyTransition {
        let transition = AnyTransition.asymmetric(
            insertion: AnyTransition.move(edge: .trailing),
            removal: AnyTransition.move(edge: .leading)
        )
        return transition
    }
    
    static var reverseSlide: AnyTransition {
        let transition = AnyTransition.asymmetric(
            insertion: AnyTransition.move(edge: .leading),
            removal: AnyTransition.move(edge: .trailing)
        )
        
        return transition
    }
}

Incorrect Transition

George B
  • 918
  • 3
  • 15
  • 24
  • 1
    I don't know if this is the answer but it is suspicious to me that you set `isReverse` in both the handler and the button closure when going back. In fact, it is not clear to me that you need to bind it to the views at all since you can change it in the `goToNextView ` and `goBack` button handlers. – JeremyP Mar 31 '23 at 07:39
  • Yes, you are correct, but the problem remains the same. – George B Mar 31 '23 at 07:59
  • What happens if you don't make isReverse an `@State` variable at all? There might be some kind of weird race condition going on because both state variables changing will trigger a redraw. – JeremyP Mar 31 '23 at 08:59
  • @JeremyP it doesn't seem to be caused by having 2 `@State` variables, even if you move `isReverse` to be an associated value on the `NavigationViewType` cases and hence only have a single `@State`, the issue still persists – Dávid Pásztor Mar 31 '23 at 09:41
  • @DávidPásztor I've built a simple application using the code and also looked at some animation documentation. I think the underlying problem is that the old view is going out the wrong way it. e.g. Go from view 2 to view 3, view 3 slides in from the right. Then go 3 to 2 and it slides out to the left instead of to the right. – JeremyP Apr 01 '23 at 13:46

0 Answers0