SwiftUI 导航:为什么视图主体中的 Timer.publish() 会破坏导航堆栈
SwiftUI Navigation: why is Timer.publish() in View body breaking nav stack
这里是 SwiftUI n00b。我正在尝试使用 NavigationView
和 NavigationLink
进行一些非常简单的导航。在下面的示例中,我已经隔离了 3 级导航。第一层只是一个link到第二层,第二层到第三层,第三层是一个文本输入框。
在二级视图生成器中,我有一个
private let timer = Timer.publish(every: 2, on: .main, in: .common)
当我导航到第 3 级时,只要我开始在文本框中输入内容,我就会导航回第 2 级。
为什么?
一个我不明白的可能线索。第二层的print(Self._printChanges())
显示
NavLevel2: @self changed.
当我开始在第 3 级文本框中输入内容时。
当我删除这个计时器声明时,问题就消失了。或者,当我将我在第 3 级使用的 @EnvironmentObject
修改为 @State
时,问题就消失了。
所以试图了解这里发生了什么,如果这是一个错误,如果不是错误,为什么它会这样。
这是完整的 ContentView
构建代码,用于回购此
import SwiftUI
class AuthDataModel: ObservableObject {
@Published var someValue: String = ""
}
struct NavLevel3: View {
@EnvironmentObject var model: AuthDataModel
var body: some View {
print(Self._printChanges())
return TextField("Level 3: Type Something", text: $model.someValue)
// Replacing above with this fixes everything, even when the
// below timer is still in place.
// (put this decl instead of @EnvironmentObject above
// @State var fff: String = ""
// )
// return TextField("Level 3: Type Something", text: $fff)
}
}
struct NavLevel2: View {
// LOOK HERE!!!! Removing this declaration fixes everything.
private let timer = Timer.publish(every: 2, on: .main, in: .common)
var body: some View {
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
}
struct ContentView: View {
@StateObject private var model = AuthDataModel()
var body: some View {
print(Self._printChanges())
return NavigationView {
NavigationLink(destination: NavLevel2())
{
Text("Level 1")
}
}
.environmentObject(model)
}
}
首先,如果您从 ContentView 的模型声明中删除 @StateObject
,它将起作用。
您不应将整个模型设置为根视图的状态。
如果您这样做,在任何已发布的 属性 的每次更改中,您的整个层次结构都将被重建。您会同意,如果您在文本字段中键入更改,您不希望在每个字母处重建完整的 UI。
现在,关于您描述的行为,这很奇怪。
鉴于上面所说的,看起来当你键入时,整个视图被重建,正如预期的那样,因为你的模型是一个 @State
对象,但是这个非托管计时器破坏了重建。我没有真正的线索来解释它, 但我有一条规矩要避免它 ;)
规则:
您不应在视图生成器中创建计时器。请记住,swiftUI 视图是构建器,而不是我们之前表示的 'views'。具体视图对象由 'body' 函数返回。
如果您暂停创建计时器,您会注意到一旦显示根视图就会调用您的计时器。 (来自 NavigationLink(destination: NavLevel2())
这可能不是您所期望的。
如果您在正文中移动计时器创建,它将起作用,因为计时器是在创建视图时创建的。
var body: some View {
var timer = Timer.publish(every: 2, on: .main, in: .common)
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
然而,这通常也不是正确的方法。
您应该创建计时器:
在.appear
处理程序中,保留引用,
并取消 .disappear
处理程序中的计时器。
在为异步任务保留的 .task
处理程序中。
我个人只在视图构建器结构中声明包装值(@State
、@Binding
、..),或者我用作条件的非常简单的基元变量(Bool、Int、..)在构建视图时。
我将所有功能性内容保存在主体或处理程序中。
要在输入 TextField
时停止返回上一视图,请将 .navigationViewStyle(.stack)
添加到 NavigationView
在 ContentView
.
这是我用来测试答案的代码:
import SwiftUI
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject var model = AuthDataModel()
var body: some View {
NavigationView {
NavigationLink(destination: NavLevel2()){
Text("Level 1")
}
}.navigationViewStyle(.stack) // <--- here the important bit
.environmentObject(model)
}
}
class AuthDataModel: ObservableObject {
@Published var someValue: String = ""
}
struct NavLevel3: View {
@EnvironmentObject var model: AuthDataModel
var body: some View {
TextField("Level 3: Type Something", text: $model.someValue)
}
}
struct NavLevel2: View {
@EnvironmentObject var model: AuthDataModel
@State var tickCount: Int = 0 // <-- for testing
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationLink(destination: NavLevel3()) {
Text("Level 2 tick: \(tickCount)")
}
.onReceive(timer) { val in // <-- for testing
tickCount += 1
}
}
}
这里是 SwiftUI n00b。我正在尝试使用 NavigationView
和 NavigationLink
进行一些非常简单的导航。在下面的示例中,我已经隔离了 3 级导航。第一层只是一个link到第二层,第二层到第三层,第三层是一个文本输入框。
在二级视图生成器中,我有一个
private let timer = Timer.publish(every: 2, on: .main, in: .common)
当我导航到第 3 级时,只要我开始在文本框中输入内容,我就会导航回第 2 级。
为什么?
一个我不明白的可能线索。第二层的print(Self._printChanges())
显示
NavLevel2: @self changed.
当我开始在第 3 级文本框中输入内容时。
当我删除这个计时器声明时,问题就消失了。或者,当我将我在第 3 级使用的 @EnvironmentObject
修改为 @State
时,问题就消失了。
所以试图了解这里发生了什么,如果这是一个错误,如果不是错误,为什么它会这样。
这是完整的 ContentView
构建代码,用于回购此
import SwiftUI
class AuthDataModel: ObservableObject {
@Published var someValue: String = ""
}
struct NavLevel3: View {
@EnvironmentObject var model: AuthDataModel
var body: some View {
print(Self._printChanges())
return TextField("Level 3: Type Something", text: $model.someValue)
// Replacing above with this fixes everything, even when the
// below timer is still in place.
// (put this decl instead of @EnvironmentObject above
// @State var fff: String = ""
// )
// return TextField("Level 3: Type Something", text: $fff)
}
}
struct NavLevel2: View {
// LOOK HERE!!!! Removing this declaration fixes everything.
private let timer = Timer.publish(every: 2, on: .main, in: .common)
var body: some View {
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
}
struct ContentView: View {
@StateObject private var model = AuthDataModel()
var body: some View {
print(Self._printChanges())
return NavigationView {
NavigationLink(destination: NavLevel2())
{
Text("Level 1")
}
}
.environmentObject(model)
}
}
首先,如果您从 ContentView 的模型声明中删除 @StateObject
,它将起作用。
您不应将整个模型设置为根视图的状态。
如果您这样做,在任何已发布的 属性 的每次更改中,您的整个层次结构都将被重建。您会同意,如果您在文本字段中键入更改,您不希望在每个字母处重建完整的 UI。
现在,关于您描述的行为,这很奇怪。
鉴于上面所说的,看起来当你键入时,整个视图被重建,正如预期的那样,因为你的模型是一个 @State
对象,但是这个非托管计时器破坏了重建。我没有真正的线索来解释它, 但我有一条规矩要避免它 ;)
规则:
您不应在视图生成器中创建计时器。请记住,swiftUI 视图是构建器,而不是我们之前表示的 'views'。具体视图对象由 'body' 函数返回。
如果您暂停创建计时器,您会注意到一旦显示根视图就会调用您的计时器。 (来自 NavigationLink(destination: NavLevel2())
这可能不是您所期望的。
如果您在正文中移动计时器创建,它将起作用,因为计时器是在创建视图时创建的。
var body: some View {
var timer = Timer.publish(every: 2, on: .main, in: .common)
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
然而,这通常也不是正确的方法。
您应该创建计时器:
在
.appear
处理程序中,保留引用, 并取消.disappear
处理程序中的计时器。在为异步任务保留的
.task
处理程序中。
我个人只在视图构建器结构中声明包装值(@State
、@Binding
、..),或者我用作条件的非常简单的基元变量(Bool、Int、..)在构建视图时。
我将所有功能性内容保存在主体或处理程序中。
要在输入 TextField
时停止返回上一视图,请将 .navigationViewStyle(.stack)
添加到 NavigationView
在 ContentView
.
这是我用来测试答案的代码:
import SwiftUI
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject var model = AuthDataModel()
var body: some View {
NavigationView {
NavigationLink(destination: NavLevel2()){
Text("Level 1")
}
}.navigationViewStyle(.stack) // <--- here the important bit
.environmentObject(model)
}
}
class AuthDataModel: ObservableObject {
@Published var someValue: String = ""
}
struct NavLevel3: View {
@EnvironmentObject var model: AuthDataModel
var body: some View {
TextField("Level 3: Type Something", text: $model.someValue)
}
}
struct NavLevel2: View {
@EnvironmentObject var model: AuthDataModel
@State var tickCount: Int = 0 // <-- for testing
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationLink(destination: NavLevel3()) {
Text("Level 2 tick: \(tickCount)")
}
.onReceive(timer) { val in // <-- for testing
tickCount += 1
}
}
}