[英]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.我有一个登录屏幕,我使用与 state 变量绑定的隐藏NavigationLink
以编程方式推送到下一个屏幕。 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
: App
:
var body: some Scene {
WindowGroup {
NavigationView {
LaunchView()
}
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
.environmentObject(cache)
}
}
LaunchView
: 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
:这是我的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
. LoginView
本身是NavigationView
的子级。
The HomeView
is really simple: 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)
}
}
What's going wrong here?这里出了什么问题?
I've realized that the issue does not occur, when I replace LaunchView()
in App
with LoginView()
directly.当我直接将App
中的LaunchView()
替换为LoginView()
时,我意识到该问题不会发生。 Not sure how this is related...不知道这有什么关系...
As Tushar pointed out below, replacing destination: destinationView
with destination: LoginView()
fixes the problem – but obviously lacks required functionality.正如 Tushar 在下面指出的那样,将destination: destinationView
替换为destination: LoginView()
可以解决问题——但显然缺少所需的功能。
So I played around with that and now understand what's going on:所以我玩弄了它,现在明白发生了什么:
LaunchView
is rendered LaunchView
被渲染LaunchView
finds there's no user data yet, so pushes to LoginView
LaunchView
发现还没有用户数据,所以推送到LoginView
LoginView
pushes to HomeView
在用户交互时, LoginView
推送到HomeView
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
.此时, LaunchView
内的NavigationLink
再次被调用(idk 为什么但断点显示了这一点),并且由于现在有用户数据,它呈现HomeView
而不是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.这就是为什么我们只看到一次推送 animation,而LoginView
成为HomeView
没有任何推送 animation,b/c 它基本上被替换了。
So now the objective is preventing LaunchView
's NavigationLink
to re-render its destination view.所以现在的目标是阻止LaunchView
的NavigationLink
重新渲染其目标视图。
I was finally able to resolve the issue thanks to Tushar 's help in the comments .多亏了Tushar 在评论中的帮助,我终于能够解决这个问题。
The main problem lies in the fact I didn't understand how the environment object triggers re-renders.主要问题在于我不明白 object 环境如何触发重新渲染。 Here's what was going on:这是发生了什么:
LaunchView
has the environment object cache
, which is changed in LoginView
, when we set cache.user = user
. LaunchView
有环境 object cache
,当我们设置cache.user = user
时,它在LoginView
中发生了变化。LaunchView
to re-render its body.这会触发LaunchView
重新渲染其主体。nil
after login, on each re-render, the user would be fetched from the API via getUser()
.由于登录后访问令牌不nil
,因此在每次重新渲染时,都会通过getUser()
从 API 获取用户。shouldPush
is set to true
不管 api 调用是否产生有效用户这一事实, shouldPush
设置为true
LaunchView
's body is rendered again and the destinationView
is computed again LaunchView
的 body 再次渲染, destinationView
再次计算HomeView
因为现在用户确实有数据,所以计算出来的视图变成了HomeView
LoginView
becoming the HomeView
w/o any push – it's being replaced.这就是为什么我们看到LoginView
在没有任何推送的情况下变成HomeView
- 它正在被替换。LoginView
pushes to HomeView
, but since that view is already presented, it pops back to its first instance同时, LoginView
推送到HomeView
,但由于该视图已经呈现,它会弹回它的第一个实例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:为此,我们可以将其设为状态管理变量并在getUser
api 调用的响应中手动设置它:
Excerpt from LaunchView
:摘自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.