繁体   English   中英

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

[英]NavigationLink pushes twice, then pops once

我有一个登录屏幕,我使用与 state 变量绑定的隐藏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的子级。

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再次被调用(idk 为什么但断点显示了这一点),并且由于现在用户数据,它呈现HomeView而不是LoginView

这就是为什么我们只看到一次推送 animation,而LoginView成为HomeView没有任何推送 animation,b/c 它基本上被替换了。

所以现在的目标是阻止LaunchViewNavigationLink重新渲染其目标视图。

多亏了Tushar 在评论中的帮助,我终于能够解决这个问题。

问题

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

  1. LaunchView有环境 object cache ,当我们设置cache.user = user时,它在LoginView中发生了变化。
  2. 这会触发LaunchView重新渲染其主体。
  3. 由于登录后访问令牌nil ,因此在每次重新渲染时,都会通过getUser()从 API 获取用户。
  4. 不管 api 调用是否产生有效用户这一事实, shouldPush设置为true
  5. LaunchView的 body 再次渲染, destinationView再次计算
  6. 因为现在用户确实有数据,所以计算出来的视图变成了HomeView
    这就是为什么我们看到LoginView在没有任何推送的情况下变成HomeView - 它正在被替换。
  7. 同时, 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]
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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