从目标视图更新 SwiftUI 列表数据会导致意外行为

Posted

技术标签:

【中文标题】从目标视图更新 SwiftUI 列表数据会导致意外行为【英文标题】:SwiftUI List data update from destination view causes unexpected behaviour 【发布时间】:2021-05-09 18:26:01 【问题描述】:

我遇到了这种相当常见的情况,List 将来自@ObservedObject 数据存储的一些项目显示为NavigationLinks

在选择NavigationLink 时,会显示一个DetailView。这个视图有一个简单的 Toggle 连接到其 ViewModel 类上的 @Published 变量。

DetailView 出现 (onAppear:) 时,它的 View Model 将控制 Toggle 的已发布 var 设置为 true 并触发一个异步请求,该请求将更新 主数据存储,导致上一个屏幕中的List也更新。

问题在于,当这种情况发生时(List 是从 Detail View 中触发的操作中重新加载),DetailViewModel 的多个实例似乎被保留,并且 DetailView 开始从错误的发布者接收事件>.

在第一次到达详细信息屏幕时,行为是正确的(如下面的代码所示),切换设置为 true 并且 商店 更新,但是,在导航回到List,然后再回到另一个DetailView,在出现时切换设置为true,但这次当重新加载store的代码执行时,切换设置回false .

我的理解是,当重新加载 List 并创建新的 DetailView 和 ViewModel(作为 NavigationLink 的目标)时,来自控制 ToggleisOn 变量的初始值(即false) 以某种方式触发了对当前显示屏幕的Toggle 的更新。

我错过了什么吗?

import SwiftUI
import Combine

class Store: ObservableObject 
    static var shared: Store = .init()
    @Published var items = ["one", "two"]
    private init()  


struct ContentView: View 
    @ObservedObject var store = Store.shared
    
    var body: some View 
        NavigationView 
            List(store.items, id: \.self)  item in
                NavigationLink(item, destination: ItemDetailView())
            
        
    


struct ItemDetailView: View 
    @ObservedObject var viewModel = ItemDetailViewModel()
    
    var body: some View 
        VStack 
            Toggle("A toggle", isOn: $viewModel.isOn)
            Text(viewModel.title)
            Spacer()
           .onAppear(perform: viewModel.onAppear)
    


class ItemDetailViewModel: ObservableObject 
    @ObservedObject var store: Store = .shared
    @Published var isOn = false

    var title: String 
        store.items[0]
    
    
    func onAppear() 
        isOn = true
        asyncOperationThatUpdatesTheStoreData()
    
    
    private func asyncOperationThatUpdatesTheStoreData() 
        DispatchQueue.main.asyncAfter(deadline: .now() + 2)  [weak self] in
            self?.store.items = ["three", "four"]
        
    

【问题讨论】:

【参考方案1】:

您控制生命周期和对象的方式不是此 UI 框架中的模式。 VieModel 不会神奇地重新发布单例值类型;它将使用该值进行实例化,然后改变其状态,而无需再次检查共享实例,除非它被重建。

class Store: ObservableObject 
    static var shared: Store = .init()

struct ContentView: View 
    @ObservedObject var store = Store.shared
struct ItemDetailView: View 
    @ObservedObject var viewModel = ItemDetailViewModel()

class ViewModel: ObservableObject 
    @Published var items: [String] = Store.shared.items   

有很多潜在的可行模式。例如:

    创建class RootStore: ObservableObject 并将其作为@StateObject 存放在App 中。 StateObject 的生命周期是该视图层次结构的生命周期。您可以 (a) 通过 .environmentObject(store) 直接将其公开给子视图,或者 (b) 创建一个容器对象,例如提供 ViewModels 的工厂,并通过环境将其传递给视图,或者 (c) 执行a 和 b 都有。

    如果您在另一个类中引用存储,请保持弱引用 weak var store: Store?。如果您在@Published 上保持该RootStore 中的状态,那么您可以直接订阅该发布者或RootStore 自己的ObjectWillChangePublisher。您还可以使用 CurrentValueSubject 等其他 Combine 发布者来实现与 @Published 相同的效果。

代码示例见

Setting a StateObject value from child view causes NavigationView to pop all views

SwiftUI @Published Property Being Updated In DetailView

【讨论】:

以上是关于从目标视图更新 SwiftUI 列表数据会导致意外行为的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI @Published 属性正在 DetailView 中更新

SwiftUI:关闭视图导致索引超出范围错误

SwiftUI 更新降低了金属窗的 FPS

合并SwiftUI远程获取数据-ObjectBinding不会更新视图

如何以编程方式从 SwiftUI 列表中删除行并刷新列表视图?

尝试更新表单提交 SwiftUI 的先前视图