NavigationLink 推送两次,然后弹出一次

NavigationLink pushes twice, then pops once

我有一个登录屏幕,在该屏幕上,我使用绑定到状态变量的隐藏 NavigationLink 以编程方式推送到下一个屏幕。推送有效,但是好像是推送两次弹出一次,正如你在这个屏幕录像上看到的:

这是我的视图层次结构:

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)
    }
}

这是我的 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)
    }
}

LoginView本身就是NavigationView的child。

HomeView其实很简单:

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)

    }
}

这里出了什么问题?


更新:

当我直接将 App 中的 LaunchView() 替换为 LoginView() 时,我发现问题 不会 发生。不确定这有什么关系...


更新 2:

正如 Tushar 在下面指出的那样,将 destination: destinationView 替换为 destination: LoginView() 可以解决问题——但显然缺少所需的功能。
所以我试了一下,现在明白发生了什么事了:

  1. LaunchView 被渲染
  2. LaunchView发现还没有用户数据,所以推送到LoginView
  3. 根据用户交互,LoginView 推送到 HomeView
  4. 此时,LaunchView里面的NavigationLink又被调用了(不知道为什么不过是一个断点显示了这个),并且由于现在 用户数据,它呈现 HomeView 而不是 LoginView.

这就是为什么我们只看到一个推送动画,LoginView 变成了 HomeView w/o 任何推送动画,b/c 它被 替换了,本质上。

所以现在 objective 正在阻止 LaunchViewNavigationLink 到 re-render 它的目标视图。

由于 Tushar's help ,我终于能够解决问题。

问题

主要问题在于我不明白环境对象是如何触发重新渲染的。这是发生了什么:

  1. LaunchView 具有环境对象 cache,当我们设置 cache.user = user.
  2. 时,它在 LoginView 中发生了变化
  3. 这会触发 LaunchView 重新渲染其主体。
  4. 因为登录后访问令牌是而不是 nil,在每次重新渲染时,用户将通过[=从API获取17=].
  5. 忽略 api 调用是否产生有效用户这一事实,shouldPush 设置为 true
  6. LaunchView 的主体再次渲染,destinationView 再次计算
  7. 因为现在用户确实有数据,计算视图变成HomeView
    这就是为什么我们看到 LoginView 变成 HomeView w/o 任何推送 - 它正在被替换。
  8. 同时,LoginView 推送到 HomeView,但由于该视图已经呈现,它会弹回到它的第一个实例

解决方案

要解决此问题,我们需要使 属性 不是 计算,以便它仅在我们需要时更改。为此,我们可以将其设为状态管理变量并在 getUser api 调用的响应中手动设置它:

摘自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]
}