Using two NavigationLink in a View

Hello fellows,

I have been trying to use TCA in my SwiftUI project. This is my first time with SwiftUI project.

I have a WelcomeView that has two buttons Signup" and Login. I want to use NavgiationView to show those screens.

The problem is after showing SignupView by NavigationLink from WelcomeView, a change I make on the SignupView screen (for example, filling in the email information) triggers navigation pop event (shows WelcomeView screen again).

Here is the NavigationLink code I use in WelcomeView;

@ViewBuilder
    func prepareSignupButton(
        with viewStore: ViewStore<Welcome.State, Welcome.Action>
    ) -> some View {
        NavigationLink(
            destination: IfLetStore(
                self.store.scope(
                    state: \.signupState,
                    action: Welcome.Action.signupAction
                ),
                then: SignupView.init(store:)
            ),
            isActive: viewStore.binding(
                get: \.isSignupNavigationActive,
                send: Welcome.Action.setSignupNavigation(isActive:)
            )
        ) {
            PrimaryButton(title: "Signup")
        }
    }
    
    @ViewBuilder
    func prepareLoginButton(
        with viewStore: ViewStore<Welcome.State, Welcome.Action>
    ) -> some View {
        NavigationLink(
            destination: IfLetStore(
                self.store.scope(
                    state: \.loginState,
                    action: Welcome.Action.loginAction
                ),
                then: LoginView.init(store:)
            ),
            isActive: viewStore.binding(
                get: \.isLoginNavigationActive,
                send: Welcome.Action.setLoginNavigation(isActive:)
            )
        ) {
            PrimaryButton(title: "Login")
        }
    }

Welcome core

struct Welcome {
    struct State: Equatable {
        var signupState: Signup.State? = nil
        var loginState: Login.State? = nil

        var isSignupNavigationActive: Bool = false
        var isLoginNavigationActive: Bool = false
        
        enum Route: Equatable {
            case signup
            case login
        }

    }
    
    enum Action: Equatable {
        case onAppear
        case onClickContinueAsGuest

        /// Signup
        case signupAction(_ action: Signup.Action)
        case setSignupNavigation(isActive: Bool)
        
        case loginAction(_ action: Login.Action)
        case setLoginNavigation(isActive: Bool)
    }
    
    struct Environment {
        let mainQueue: AnySchedulerOf<DispatchQueue> = .main
    }
    
    static let reducer = Reducer.combine(
        Reducer<State, Action, Environment> {state, action, environment in
            switch action {
            case .onAppear:
                return .none
            case .onClickContinueAsGuest:
                return .none
            case .setSignupNavigation(isActive: true):
                state.isSignupNavigationActive = true
                state.signupState = .init()
                return .none
            case .setSignupNavigation(isActive: false):
                state.isSignupNavigationActive = false
                state.signupState = nil
                return .none
            case .signupAction(let signupAction):
                return .none
                
            case .loginAction:
                return .none
            case .setLoginNavigation(let isActive):
                return .none
            }
        },
        
        Signup
            .reducer
            .optional()
            .pullback(
                state: \.signupState,
                action: /Action.signupAction,
                environment: { env in
                    .init()
                }
            ),
        
        Login
            .reducer
            .optional()
            .pullback(
                state: \.loginState,
                action: /Action.loginAction,
                environment: { env in
                    .init()
                }
            )
    )
}

struct Signup {
    struct State: Equatable {
        var user: User = .init()
        var isTermsChecked: Bool = false
        var isSignupButtonAvailable: Bool = false
        
        struct User: Equatable {
            var email: String = .empty
            var phone: String = .empty
            var countryCode: String = .empty
            var name: String = .empty
            var password: String = .empty
        }
    }
    
    enum Action: Equatable {
        case onAppear
        case emailFieldChanged(_ email: String)
        case phoneFieldChanged(_ phone: String)
        case countryCodeChanged(_ countryCode: String)
        case nameFieldChanged(_ name: String)
        case passwordFieldChanged(_ password: String)
        case termsCheckStateChanged(isChecked: Bool)
        case signupButtonAction(isActive: Bool)
    }
    
    struct Environment {
        let mainQueue: AnySchedulerOf<DispatchQueue> = .main
    }
    
    static let reducer = Reducer<State, Action, Environment> {state, action, environment in
        switch action {
        case .onAppear:
            return .none
        case .emailFieldChanged(let text):
            state.user.email = text
            return .none
        case .phoneFieldChanged(let text):
            state.user.phone = text
            return .none
        case .countryCodeChanged(let countryCode):
            state.user.countryCode = countryCode
            return .none
        case .nameFieldChanged(let text):
            state.user.name = text
            return .none
        case .passwordFieldChanged(let text):
            state.user.password = text
            return .none
        case .termsCheckStateChanged(isChecked: true):
            state.isTermsChecked = true
            return .none
        case .termsCheckStateChanged(isChecked: false):
            state.isTermsChecked = true
            return .none
        case .signupButtonAction(isActive: true):
            state.isSignupButtonAvailable = true
            return .none
        case .signupButtonAction(isActive: false):
            state.isSignupButtonAvailable = false
            return .none
        }
    }
}

When case .emailFieldChanged(let text): triggered, NavigationView triggers pop method.

The problem is when I assign a value to state (in Signup), navigation push the screen back!

I'm not quite sure I understand what the problem is here? Please could you elaborate on what the issue is.

Thanks

Hello!

I am showing the SignupView from WelcomeView screen but when I assign or change any value in Signup.reducer, it is sending me back to WelcomeView. Somehow something triggers this method in Welcome

case .setSignupNavigation(isActive: false):
                state.isSignupNavigationActive = false
                state.signupState = nil
                return .none

Ah thanks for the clarification. :+1:

  1. As I remember there were problems with SwiftUI navigation when View have exact 2 NavigationLinks. Try to add
var body: some View {
    YourCode()
        .background(NavigationLink(EmptyView()) {})
}
  1. I had similar experience also just with plain single navigation link. In my opinion, whole navigation in SwiftUI is unusable and I switched to UIKit-based navigation. I recreated project with pure SwiftUI and noticed, that when you use NavigationLink and it pushes another screen, NaigationLink binding resets to false, but new view still appears on the screen. But in TCA when binding resets to false, you also clears your state, which again calls redraw and pushed screen closes.

So I abandoned my hope that I will use SwiftUI for everything in single project if we are speaking about iPhone+iPad+Catalyst. You will face a lot more problems with SplitViews in iPad so I think its better to use good old UIViewController.present