SwiftUI 导航:为什么视图主体中的 Timer.publish() 会破坏导航堆栈

SwiftUI Navigation: why is Timer.publish() in View body breaking nav stack

这里是 SwiftUI n00b。我正在尝试使用 NavigationViewNavigationLink 进行一些非常简单的导航。在下面的示例中,我已经隔离了 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) 添加到 NavigationViewContentView.

这是我用来测试答案的代码:

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
        }
    }
}