One way to handle this is with an @Environment object created from a BaseViewModel. The way that this works is to essentially control the state of the presented view from a BaseView or a view controller. I'll attempt to simplify it for you the best I can.
class BaseViewModel: ObservableObject { @Published var baseView: UserFlow = .loading init() { //Handle your condition if already logged in, change //baseView to whatever you need it to be. } enum UserFlow { case loading, onboarding, login, home } }
Once you've setup your BaseViewModel you'll want to use it, I use it in a switch statement with a binding to an @EnvironmentObject so that it can be changed from any other view.
struct BaseView: View { @EnvironmentObject var appState: BaseViewModel var body: some View { Group { switch appState.userFlow { case .loading: LoadingView() case .onboarding: Text("Not Yet Implemented") case .login: LandingPageView() case .home: BaseHomeScreenView().environmentObject(BaseHomeScreenViewModel()) } } } }
Your usage, likely at the end of your register/login flow, will look something like this.
struct LoginView: View { @EnvironmentObject var appState: BaseViewModel var body: some View { Button(action: {appState = .home}, label: Text("Log In")) } }
So essentially what's happening here is that you're storing your app flow in a particular view which is never disposed of. Think of it like a container. Whenever you change it, it changes the particular view you want to present. The especially good thing about this is that you can build a separate navigation hierarchy without the use of navigation links, if you wanted.