【问题标题】:NavigationLink pushes twice, then pops onceNavigationLink 推送两次,然后弹出一次
【发布时间】:2021-05-13 08:32:50
【问题描述】:

我有一个登录屏幕,我使用与状态变量绑定的隐藏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 再次被调用(我知道为什么,但是断点显示了这一点),因为那里是 现在用户数据,它呈现 HomeView 而不是 LoginView

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

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

【问题讨论】:

  • 您可以尝试在您的 LaunchView 中将目的地硬编码到 LoginView() 以查看会发生什么?
  • 我猜问题在于 API 调用,以及它们如何更新您的模型状态,以及正在观察您的模型以执行刷新的视图。你需要更仔细地检查。如果我以最简单的方式测试您的代码,而不依赖于“缓存类”和“访问令牌”值,并且只使用单个 @State 属性来切换状态,那么一切正常。
  • 据我了解您的“缓存”是@EnvironmentObject 属性,因此一旦该模型的任何属性值发生更改,所有标有 EnvironmentObject 的视图都将重新呈现主体。
  • 哦对了,所以每次cache.user 更改时都会调用getUser(),对吧?然后结果触发通过NavigationLink重复推送,从而导致问题。
  • 是的,你没看错。

标签: swift swiftui navigation


【解决方案1】:

感谢Tushar的帮助in the comments,我终于能够解决这个问题。

问题

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

  1. LaunchView 具有环境对象cache,在LoginView 中更改,当我们设置cache.user = user 时。
  2. 这会触发 LaunchView 重新渲染其主体。
  3. 由于登录后访问令牌不是nil,因此在每次重新渲染时,都会通过getUser() 从API 中获取用户。
  4. 不管该 api 调用是否产生有效用户这一事实,shouldPush 设置为 true
  5. LaunchView 的主体再次渲染,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]
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-01-16
    • 2020-04-04
    • 2022-08-09
    • 1970-01-01
    • 2020-05-27
    • 1970-01-01
    • 2021-08-12
    • 2015-08-14
    相关资源
    最近更新 更多