SwiftUI View 不会更新对已发布对象的已发布属性的异步更改

SwiftUI View not updating on async change to published properties of Observed Object

我有以下SwiftUI视图:

struct ProductView: View {
@ObservedObject var productViewModel: ProductViewModel


var body: some View {
    VStack {
        ZStack(alignment: .top) {
            if(self.productViewModel.product != nil) {
                URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
            }
            else {
                Image("loading")
            } 
        }
    }
}

观察 ProductViewModel

class ProductViewModel: ObservableObject {

@Published var selectedColor: UIColor = .white

@Published var product: Product?


private var cancellable: AnyCancellable!

init(productFuture: Future<Product, Never>) {

    self.cancellable = productFuture.sink(receiveCompletion: { comp in
            print(comp)
        }, receiveValue: { product in
            
            self.product = product
            print(self.product)  // this prints the expected product. The network call works just fine


        })


}

Product 是一个 Swift 结构,包含几个字符串属性:

struct Product {
    let id: String
    let imageurl: String
    let price: String

}

它是从远程 API 获取的。执行提取 returns Combine 未来并将其传递给视图模型的服务,如下所示:

let productFuture = retrieveProduct(productID: "1")
let productVM = ProductViewModel(productFuture: productFuture)
let productView = ProductView(productViewModel: productViewModel)


func retrieveProduct(productID: String) -> Future<Product, Never>{
    
    let future = Future<Product, Never> { promise in

    //  networking logic that fetches the remote product,  once it finishes the success callback is invoked


        promise(.success(product))
    }

    return future
}

为了简洁起见,我排除了网络和错误处理逻辑,因为它与手头的案例无关。要尽快重现这一点,只需使用一些虚拟值初始化模拟产品并将其传递给成功回调,延迟如下:

let mockproduct = Product(id: "1", imageurl: "https://exampleurl.com", price: "")

DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
    promise(.success(mockproduct))
})

产品通过网络到达后,将分配给已发布的产品属性。 抓取工作正常,正确的值分配给已发布的 属性。显然,这发生在视图创建之后,因为网络调用需要一些时间。但是,即使已发布的对象发生更改,视图也永远不会更新。

当我直接通过视图模型初始化器而不是未来传递产品时,它按预期工作并且视图显示正确的产品。

关于视图在通过组合未来异步更新时为什么不对视图模型状态的变化做出反应的任何建议?

编辑:当我问这个问题时,我将 ProductViewModel + ProductView 嵌套在另一个视图中。所以基本上 productview 只是更大的 CategoryView 的一部分。 CategoryViewmodel 在专用方法中初始化了 ProductViewModel 和 ProductView:

func createProductView() -> AnyView {
    let productVM = productViewModels[productIndex]
    return AnyView(ProductView(productViewModel: productVM))
}

然后在每次更新时由 CategoryView 调用。我想这导致嵌套 ProductViewModel 中的 Published 变量无法正确更新,因为每次更新时都会重建从 CategoryView 向下的视图层次结构。因此,每次更新都会调用 createProductView 方法,从而对 ProductView + ProductViewModel 进行全新的初始化。

也许对 SwiftUI 有更多经验的人可以对此发表评论。

在嵌套视图中嵌套可观察对象通常不是一个好主意,或者有没有办法使这项工作不是反模式?

如果没有,当你有嵌套视图且每个视图都有自己的状态时,你通常如何解决这个问题?

我一直在迭代这样的模式,以找到我认为最有效的模式。不确定到底是什么问题。我的直觉表明 SwiftUI 在 != nil 部分进行更新时遇到问题。

这是我一直在使用的模式,它一直有效。

  1. 为网络逻辑中的状态定义一个枚举
public enum NetworkingModelViewState {
    case loading
    case hasData
    case noResults
    case error
}
  1. 将枚举添加为“视图模型”中的变量
class ProductViewModel: ObservableObject {

    @Published public var state: NetworkingModelViewState = .loading

}
  1. 随着网络的进展更新状态
    self.cancellable = productFuture.sink(receiveCompletion: { comp in
            print(comp)
        }, receiveValue: { product in
            
            self.product = product
            self.state = NetworkingModelViewState.hasData
            print(self.product)  // this prints the expected product. The network call works just fine


        })
  1. 现在根据枚举值
  2. 在你的SwiftUI中做出决定
            if(self.productViewModel.state == NetworkingModelViewState.hasData) {
                URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
            }
            else {
                Image("loading")
            }

沉思 ~ 很难调试声明式框架。它们很强大,我们应该继续学习它们,但要注意被卡住。移动 SwiftUI 迫使我真正考虑 MVVM。我的结论是,您确实需要分离出控制您的 UI 的每个可能的变量。你不应该依赖于读取变量之外的检查。 Combine future 模式存在内存泄漏问题,Apple 将在下一个版本中修复该问题。此外,您将能够在 SwiftUI 下一个版本中切换。