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()
可以解决问题——但显然缺少所需的功能。
所以我试了一下,现在明白发生了什么事了:
LaunchView
被渲染
LaunchView
发现还没有用户数据,所以推送到LoginView
- 根据用户交互,
LoginView
推送到 HomeView
- 此时,
LaunchView
里面的NavigationLink
又被调用了(不知道为什么不过是一个断点显示了这个),并且由于现在 是 用户数据,它呈现 HomeView
而不是 LoginView
.
这就是为什么我们只看到一个推送动画,LoginView
变成了 HomeView
w/o 任何推送动画,b/c 它被 替换了,本质上。
所以现在 objective 正在阻止 LaunchView
的 NavigationLink
到 re-render 它的目标视图。
由于 Tushar's help ,我终于能够解决问题。
问题
主要问题在于我不明白环境对象是如何触发重新渲染的。这是发生了什么:
LaunchView
具有环境对象 cache
,当我们设置 cache.user = user
. 时,它在 LoginView
中发生了变化
- 这会触发
LaunchView
重新渲染其主体。
- 因为登录后访问令牌是而不是
nil
,在每次重新渲染时,用户将通过[=从API获取17=].
- 忽略 api 调用是否产生有效用户这一事实,
shouldPush
设置为 true
LaunchView
的主体再次渲染,destinationView
再次计算
- 因为现在用户确实有数据,计算视图变成
HomeView
这就是为什么我们看到 LoginView
变成 HomeView
w/o 任何推送 - 它正在被替换。
- 同时,
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]
}
我有一个登录屏幕,在该屏幕上,我使用绑定到状态变量的隐藏 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()
可以解决问题——但显然缺少所需的功能。
所以我试了一下,现在明白发生了什么事了:
LaunchView
被渲染LaunchView
发现还没有用户数据,所以推送到LoginView
- 根据用户交互,
LoginView
推送到HomeView
- 此时,
LaunchView
里面的NavigationLink
又被调用了(不知道为什么不过是一个断点显示了这个),并且由于现在 是 用户数据,它呈现HomeView
而不是LoginView
.
这就是为什么我们只看到一个推送动画,LoginView
变成了 HomeView
w/o 任何推送动画,b/c 它被 替换了,本质上。
所以现在 objective 正在阻止 LaunchView
的 NavigationLink
到 re-render 它的目标视图。
由于 Tushar's help
问题
主要问题在于我不明白环境对象是如何触发重新渲染的。这是发生了什么:
LaunchView
具有环境对象cache
,当我们设置cache.user = user
. 时,它在 - 这会触发
LaunchView
重新渲染其主体。 - 因为登录后访问令牌是而不是
nil
,在每次重新渲染时,用户将通过[=从API获取17=]. - 忽略 api 调用是否产生有效用户这一事实,
shouldPush
设置为true
LaunchView
的主体再次渲染,destinationView
再次计算- 因为现在用户确实有数据,计算视图变成
HomeView
这就是为什么我们看到LoginView
变成HomeView
w/o 任何推送 - 它正在被替换。 - 同时,
LoginView
推送到HomeView
,但由于该视图已经呈现,它会弹回到它的第一个实例
LoginView
中发生了变化
解决方案
要解决此问题,我们需要使 属性 不是 计算,以便它仅在我们需要时更改。为此,我们可以将其设为状态管理变量并在 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]
}