从子视图设置 StateObject 值会导致 NavigationView 弹出所有视图

Posted

技术标签:

【中文标题】从子视图设置 StateObject 值会导致 NavigationView 弹出所有视图【英文标题】:Setting a StateObject value from child view causes NavigationView to pop all views 【发布时间】:2021-05-08 23:34:02 【问题描述】:

我有一个应用程序以类似 redux 的模式使用 StateObject。它一直运行良好 - 直到我尝试使用可以以编程方式使用的 NavigationLinks 实现 NavigationView。

每次我尝试从子视图发送“调度”操作时,都会将子视图从导航堆栈中弹出。我想这个问题可能与我传递环境对象的位置有关,所以我将它从 NavigationView 移动到子视图。没有变化。

为什么我的视图会立即弹出?是否有一些我不知道的重绘触发?

enum NavigationTag: String 
    case page1
    case page2


struct ContentView: View 
    @StateObject private var store = AppStore(
        initialState: .init(),
        reducer: appReducer)
    @State private var linkTag: NavigationTag? = .page1
    
    var body: some View 
        let splashNavView = NavigationView 
            VStack(alignment: .center) 
                Text("Loading..." + (linkTag?.rawValue ?? "nil"))
                Text("Something went wrong. You shouldn't be seeing this.")

                NavigationLink(destination: Page1View().environmentObject(store), tag: .page1, selection: $linkTag) EmptyView()
                NavigationLink(destination: Text("page 2 view").environmentObject(store), tag: .page2, selection: $linkTag) EmptyView()

            
        

        return splashNavView
        
    

子视图

struct Page1View: View 
    @EnvironmentObject var store: AppStore
    
    var body: some View 
            ZStack 
                Color.orange
            
            .onAppear(perform: 
                store.dispatch(.floatingView(action: .setSize(width: 414, height: 590)))
            )
            .navigationBarTitle("")
            .navigationBarHidden(true)
    

结果如下:

【问题讨论】:

切换到LazyVStack vs VStack @loremipsum 似乎没有任何改变。仍然得到相同的结果。 众所周知,SwiftUI 预加载 NavigationLinks 在 SO 中有很多关于它的问题,使用 List LazyVStack 应该可以防止它,但有些解决方案需要自定义 LazyView 尝试将 LazyVStack 放在 ScrollView 中 【参考方案1】:

在模拟一个 Store 时,dispatch 会改变直接触发 ObjectWillChange 的根 @Published 状态,我可以复制您的弹出行为。如果我通过使用 CurrentValueSubject 或嵌套 ObservableObject 将状态突变与 ObjectWillChange 分开,NavigationLink 将完全按照预期执行。

我建议将全局根状态触发与强制将所有视图标记为脏视图分离。这样你就可以使用差异机制来自己触发。

也就是说,也许有一个解决方法:@loremipsum 评论。我不熟悉这一点,因为我只在 macOS 多窗格中使用 NavigationView,我发现它非常可靠。

编辑:示例

鉴于您在评论中的 Q,这是一个粗略的演示,它将您的根状态保持在 @Published 属性上。

这使您可以将旧存储传递到环境中,并正常使用它来处理您可以对每个可能的状态突变进行差异化的视图。

模拟商店和容器

class Store: ObservableObject 
    @Published var state = RootState()



struct RootState 
    var nav: NavTag = .page1
    var other: Int = 1


class Container: ObservableObject 
    let store = Store()

    func makeNavVM() -> NavVM 
        NavVM(store)
    


struct RootView: View 
    
    @StateObject private var container = Container()
    
    var body: some View 
        ContentView(vm: container.makeNavVM())
            .environmentObject(container.store)
            .environmentObject(container)
    


导航


class NavVM: ObservableObject 
    
    init(_ root: Store) 
        self.root = root
        root.$state
            .receive(on: DispatchQueue.main)
            .sink  [weak self] state in
                guard let self = self,
                      state.nav != self.tag else  return 
                self.tag = state.nav
            
            .store(in: &subs)
    
    
    @Published private(set) var tag: NavTag = .page1
    private var subs = Set<AnyCancellable>()
    private weak var root: Store?
    
    func navigate(to tag: NavTag?) 
        guard let tag = tag else  return 
        root?.state.nav = tag
    


enum NavTag: String 
    case page1
    case page2
    
    var color: Color 
        switch self 
            case .page1: return .pink
            case .page2: return .yellow
        
    
    
    var label: String 
        switch self 
            case .page1: return "Page 1"
            case .page2: return "Page 2"
        
    

用法


struct ContentView: View 
    
    @StateObject var vm: NavVM

    var body: some View 
        NavigationView 
            VStack(alignment: .center) 
                Text("Your navigation view")
                NavigationButton(tag: .page1)
                NavigationButton(tag: .page2)

            
        
        .environmentObject(vm)
    



struct Child: View 
    @EnvironmentObject var store: Store
    
    var color: Color
    var body: some View 
        VStack 
            Button  store.state.other += 1  label: 
                ZStack 
                    color
                    Text("Press to mutate state \(store.state.other)")
                
            
            NavigationLink("Go to page not in NavTag or state", destination: Child(color: .green))
        
    



/// On its own the Link-in-own-view-encapsulation
/// worked on first screen w/o navigation,
/// but once navigated the view hierarchy will pop
/// unless you stop state updates at the top of the hierarchy
struct NavigationButton: View 
    @EnvironmentObject var navVM: NavVM
    var tag: NavTag
    
    var neveractuallynil: Binding<NavTag?> 
        Binding<NavTag?>  navVM.tag 
            set:   navVM.navigate(to: $0) 
        
    
    var body: some View 
        NavigationLink(destination: Child(color: tag.color),
                       tag: tag,
                       selection: neveractuallynil ) 
                 Text(tag.label) 
            
    

演示使用视图模型对象对更新进行分区,但您也可以在视图中使用组合管道。您可能希望从您用于根状态和 UI 工厂对象的发布者中对这个通用主题进行其他变体。

【讨论】:

您的意思是将 ObservableObject 作为属性而不是结构嵌套在存储中吗?我尝试用 ObservableObject 替换导航结构,但得到了相同的结果。 没有。您希望将根状态突变与影响顶视图层次结构隔离开来。我假设您有一个准系统 RootStore,其中包含一个 @Published 状态属性、一个称为调度的函数和一些中间件。这个想法是将其放入一个容器 ObservableObject 中,该容器永远不会从任何已发布的属性中触发其发布者。带有 NavigationLinks 的顶视图只需要区分导航状态更改,而不需要其他状态更改。您可以通过 .onReceive 或订阅根状态更新但仅发布该状态的一部分的视图模型来做到这一点。 我在 SwiftUI 中构建了一些带有自己的 redux 的应用程序。但是,为了使其具有性能,差异化机制是必不可少的。大多数状态不需要在根/上级减速器中进行太多集成,因此实际上它仅相当于 CLEAN 存储/交互器/用例,但需要从每次更新中“排除”视图。因此,由于失去了惊人的日志记录和线程清晰度,我现在只在 SwiftUI 中使用 CLEAN,因为它通过在具有自己的组合管道的单独的 ObservableObjects 中保存状态来节省大量时间。至少对于我的中型应用程序及其状态模型而言,使用最少的代码即可工作。 谢谢你。后退按钮是现在唯一仍然不起作用的东西。它正在弹出所有视图。您知道是什么干扰了该功能吗? 我不熟悉 CLEAN 架构。我一定会检查出来的。听起来这是一种拆分单个商店的方法。由于您提到的原因,SwiftUI 可能会更好。【参考方案2】:

这是 NavigationView 中的一个已知错误 - 它会在某个时刻将您的 linkTag 重置为 nil,从而导致导航堆栈重置。

解决方法是将链接分成独立的子视图,如下面的演示(使用 Xcode 12.5 / ios 14.5 测试):

var body: some View 
    NavigationView 
        VStack(alignment: .center) 
            Text("Loading..." + (linkTag?.rawValue ?? "nil"))
            Text("Something went wrong. You shouldn't be seeing this.")

                Link1(linkTag: $linkTag)
                Link2(linkTag: $linkTag)
        
    .environmentObject(store)

和类似的链接

struct Link1: View 
    @Binding var linkTag: NavigationTag?
    var body: some View 
         NavigationLink(destination: Page1View(), tag: .page1, selection: $linkTag) EmptyView()
    


struct Link2: View 
    @Binding var linkTag: NavigationTag?

    var body: some View 
         NavigationLink(destination: Text("page 2 view"), tag: .page2, selection: $linkTag) EmptyView()
    

【讨论】:

我试过了,但是 linkTag 一直被设置为 nil。我很确定它与我的 StateObject 相关,因为当我删除在其上设置值的子视图上的行时,导航工作。 我注意到 macOS 多窗格上的行为并使用绑定来禁止设置 nil 值。对于您的堆栈视图,我没有发现子视图或非零绑定解决方法有效。第一个活动链接将允许状态更新而不弹出,但一旦在任何后续状态更新后导航,就会弹出导航堆栈。

以上是关于从子视图设置 StateObject 值会导致 NavigationView 弹出所有视图的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:如何在父视图中初始化新的 StateObject?

从子视图控制器呈现主视图控制器

StateObject 属性不会更新视图,但 ObservedObject 会

如何以 stateobject 作为参数初始化视图?

SwiftUI:@ObservedObject 与 @StateObject 子视图性能?

SwiftUI onChange(of: ...) 更新 StateObject