Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animate Transitions of Root View? #33

Open
rhysm94 opened this issue Sep 14, 2022 · 5 comments
Open

Animate Transitions of Root View? #33

rhysm94 opened this issue Sep 14, 2022 · 5 comments

Comments

@rhysm94
Copy link
Contributor

rhysm94 commented Sep 14, 2022

Hi there!

I've got my initial app screen driven by a coordinator, where it tries to get some data to determine if the user is logged in or not, and goes to an appropriate screen. The app starts by showing a splash screen to perform that logic, before transitioning to either the logged in screen, or the onboarding screen.

Almost everything behaves exactly as expected, with the exception of transitions. The root views never transition between each other, they only ever immediately swap.

I've tried a few things - pushing those state changes into a separate action that I return with .animate(.default) after, wrapping the state.routes = [.root(.onboarding)] in a withAnimation block, wrapping the initial action send in my splash screen with withAnimation, but none of them seem to work.

Checking in the previous issues, it seems that some people have found a solution, but the code seems more geared towards FlowStacks than TCACoordinators. I'm wondering if there is a solution, even if it's potentially a bit hacky and verbose?

FWIW, a little sketch of how my main view is set up is here:

struct MainView: View {
  let store: Store<MainCoordinatorState, MainCoordinatorAction>

  var body: some View {
    TCARouter(store) { store in
      SwitchStore(store) { screen in
        CaseLet(
          state: /MainState.splashScreen,
          action: /MainAction.splashScreen
        ) { store in
          SplashScreenView(store: store)
            .transition(.slide)
        }
        
        CaseLet(
          state: /MainState.onboarding,
          action: /MainAction.onboarding
        ) { store in
          OnboardingView(store: store)
            .transition(.slide)
        }
        
        CaseLet(
          state: /MainState.mainTab,
          action: /MainAction.mainTab
        ) { store in
          MainTabView(store: store)
            .transition(.slide)
        }
      }
    }
  }
}

Have I placed the transition in the wrong place? Is there anything else I need to change?

Thanks for a great library 😃

@johnpatrickmorgan
Copy link
Owner

Thanks @rhysm94!

I’m afk today, but I think I’ve had the same trouble previously in TCACoordinators, where it doesn’t allow for animated transitions of the root screen, that are possible in FlowStacks. I’ll try to dig into why.

In the meantime, perhaps a workaround would help. In your case, is the routes array’s count always one? If so, you might find it simpler not to use a TCARouter at all, and to just use a SwitchStore. I’m using that pattern in the example app’s Game demo here. There, I’m switching between two child coordinators and animating the transition successfully. Note that I wrap both screens in a VStack - as SwiftUI transitions don’t seem to work otherwise, even in vanilla SwiftUI.

Incidentally, I’ve tried in the past to come up with a general purpose transitioning API for FlowStacks, e.g. routes.replaceCurrentScreen(newScreen, transition: .slide), but I hit some SwiftUI oddities that scuppered my plans.

@rhysm94
Copy link
Contributor Author

rhysm94 commented Sep 20, 2022

Hi @johnpatrickmorgan - sorry for the delayed response!

Thanks for the workaround - it's not quite the case where the array count is always one, as I have a login screen to present too. Effectively, it would ideally look something like this:

On app launch, load the Splash Screen view. [.root(.splashScreen)]
Splash screen reducer checks with database/identity service to see if a user is signed in or not.
If the user is signed in, go to the main app screen. [.root(.mainScreen)]
If the user isn't signed in, go to an onboarding screen with a login button, which uses a NavigationView to push a login screen. [.root(.onboarding)]
On login button press... [.root(.onboarding), .push(.login)]

Your hint to use a standard SwitchStore helped massively - I've just had to use an invisible NavigationLink inside my Onboarding view, though I imagine I could create an Onboarding coordinator if I wanted!

Thanks again for the help, much appreciated! Frustrating that SwiftUI, even now, has these weird quirks where things don't work as you'd expect them, but it's good that there are mostly still workarounds!

@johnpatrickmorgan
Copy link
Owner

Great, glad you've reached an acceptable solution - yes, I've definitely found the transition API a bit of a head-scratcher!

@rhysm94
Copy link
Contributor Author

rhysm94 commented Sep 30, 2022

Hi again!

I'm having an issue with what might be the same problem (i.e. SwiftUI being janky) but thought I'd double check quickly.
I've got a flow in my application where the user fills in some data, then is presented with a sign-up screen, then continues with data entry.

My issue is that when I move on from Sign Up to the next screen, I want to remove the Sign Up screen from the underlying array of routes entirely so that the user can't swipe back to visit it again.
I've tried doing this a few ways, including simply calling

state.routeIDs.remove(.signUp)
state.push(.viewAfterSignUp)

which just causes an immediate, transition-less animation. I've tried swapping the order in which I do them, which does the same thing. I've also tried wrapping the above in Effect.routeWithDelaysIfUnsupported.

I've also tried adding a separate action to remove the Sign Up ID from my routeIDs array like this:

state.push(.viewAfterSignUp)
return .task {
  try? await environment.mainQueue.sleep(for: .seconds(0.5))
  return .removeSignUp
}

case .removeSignUp:
  // look up index for .signUp and remove here

but this one seems to trigger some double-presentation animation, where it presents the view after sign up normally, then immediately presents it again.

This also seems to happen if I call

routes.dismiss()
routes.push(.viewAfterSignUp)

Is there any way that you know of to more easily remove this view from the underlying navigation array?

Thanks!

@johnpatrickmorgan
Copy link
Owner

Thanks @rhysm94, this is a tricky one, without a great solution I know of...

When you do:

state.routeIDs.remove(.signUp)
state.push(.viewAfterSignUp)

After TCACoordinators reflects that change, according to SwiftUI, no navigation has occurred as there are still the same number of screens in the stack, with the same presentation styles. It instead looks like the individual screens have changed their content, hence the lack of animation.

One approach would be to have a single screen responsible for both viewAfterSignUp and signup that animates a transition between those two states using the .transition(...) API.

Another approach would be to keep the signUp in the stack, but disable the back button / swipe gestures so that it can no longer be reached.

Finally, it might work to take signUp out of the flow in the same way as the splash screen, and to animate from it to the coordinator managing signup.

I'm not sure what your stack looks like before and after the removal and push, so some of those suggestions might not be viable. And unfortunately none of those approaches are ideal. In UIKit, UINavigationController has a great setViewControllers(animated: ) API which will animate changes even if the count doesn't change. I'd hoped to achieve a similar API with FlowStacks/TCACoordinators, but I've hit a number of SwiftUI oddities when trying to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants