简体   繁体   中英

NavigationLink pushes twice, then pops once

I have a login screen on which I programmatically push to the next screen using a hidden NavigationLink tied to a state variable. The push works, but it seems to push twice and pop once, as you can see on this screen recording:

This is my view hierarchy:

App
   NavigationView
      LaunchView
         LoginView
            HomeView

App :

var body: some Scene {
    WindowGroup {
        NavigationView {
            LaunchView()
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarHidden(true)
        .environmentObject(cache)
    }
}

LaunchView :

struct LaunchView: View {
    @EnvironmentObject var cache: API.Cache
    @State private var shouldPush = API.shared.accessToken == nil
    
    func getUser() {
        [API call to get user, if already logged in](completion: { user in
            if let user = user {
                // in our example, this is NOT called
                // thus `cache.user.hasData` remains `false`
                cache.user = user
            }
            shouldPush = true
        }
    }
    
    private var destinationView: AnyView {
        cache.user.hasData
            ? AnyView(HomeView())
            : AnyView(LoginView())
    }
    
    var body: some View {
        if API.shared.accessToken != nil {
            getUser()
        }
        
        return VStack {
            ActivityIndicator(style: .medium)
            NavigationLink(destination: destinationView, isActive: self.$shouldPush) {
                EmptyView()
            }.hidden()
        }
        .navigationBarTitle("")
        .navigationBarHidden(true)
    }
}

This is a cleaned version of my LoginView :

struct LoginView: View {
    @EnvironmentObject var cache: API.Cache
    @State private var shouldPushToHome = false
   
    func login() {
        [API call to get user](completion: { user in
            self.cache.user = user
            self.shouldPushToHome = true
        })
    }
    
    var body: some View {
        VStack {
            ScrollView(showsIndicators: false) {
                // labels
                // textfields
                // ...
                PrimaryButton(title: "Anmelden", action: login)
                NavigationLink(destination: HomeView(), isActive: self.$shouldPushToHome) {
                    EmptyView()
                }.hidden()
            }
            // label
            // signup button
        }
        .navigationBarTitle("")
        .navigationBarHidden(true)
    }
}

The LoginView itself is child of a NavigationView .

The HomeView is really simple:

struct HomeView: View {
    @EnvironmentObject var cache: API.Cache

    var body: some View {
        let user = cache.user
        
        return Text("Hello, \(user.contactFirstname ?? "") \(user.contactLastname ?? "")!")
            .navigationBarTitle("")
            .navigationBarHidden(true)

    }
}

What's going wrong here?


Update:

I've realized that the issue does not occur, when I replace LaunchView() in App with LoginView() directly. Not sure how this is related...


Update 2:

As Tushar pointed out below, replacing destination: destinationView with destination: LoginView() fixes the problem – but obviously lacks required functionality.
So I played around with that and now understand what's going on:

  1. LaunchView is rendered
  2. LaunchView finds there's no user data yet, so pushes to LoginView
  3. upon user interaction, LoginView pushes to HomeView
  4. at this point, the NavigationLink inside LaunchView is called again (idk why but a breakpoint showed this), and since there is user data now, it renders the HomeView instead of the LoginView .

That's why we see only one push animation, and the LoginView becoming the HomeView w/o any push animation, b/c it's replaced , essentially.

So now the objective is preventing LaunchView 's NavigationLink to re-render its destination view.

I was finally able to resolve the issue thanks to Tushar 's help in the comments .

Problem

The main problem lies in the fact I didn't understand how the environment object triggers re-renders. Here's what was going on:

  1. LaunchView has the environment object cache , which is changed in LoginView , when we set cache.user = user .
  2. That triggers the LaunchView to re-render its body.
  3. since the access token is not nil after login, on each re-render, the user would be fetched from the API via getUser() .
  4. disregarding the fact whether that api call yields a valid user, shouldPush is set to true
  5. LaunchView 's body is rendered again and the destinationView is computed again
  6. since now the user does have data, the computed view becomes HomeView
    This is why we see the LoginView becoming the HomeView w/o any push – it's being replaced.
  7. at the same time, the LoginView pushes to HomeView , but since that view is already presented, it pops back to its first instance

Solution

To fix this, we need to make the property not computed, so that it only changes when we want it to. To do so, we can make it a state-managed variable and set it manually in the response of the getUser api call:

Excerpt from LaunchView :

// default value is `LoginView`, we could also
// set that in the guard statement in `getUser`
@State private var destinationView = AnyView(LoginView())

func getUser() {
    // only fetch if we have an access token
    guard API.shared.accessToken != nil else {
        return
    }
    API.shared.request(User.self, for: .user(action: .get)) { user, _, _ in
        cache.user = user ?? cache.user
        shouldPush = true
        // manually assign the destination view based on the api response
        destinationView = cache.user.hasData
            ? AnyView(HomeView())
            : AnyView(LoginView())
    }
}
    
var body: some View {
    // only fetch if user hasn't been fetched
    if cache.user.hasData.not {
        getUser()
    }

    return [all the views]
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM