首页 > 解决方案 > SwiftUI 视图未在对已观察对象的已发布属性的异步更改时更新

问题描述

我有以下 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 获取的。执行获取的服务返回一个组合未来并将其传递给视图模型,如下所示:

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: "$10")

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 有更多经验的人可以对此发表评论。

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

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

标签: swiftuicombineobservedobject

解决方案


我一直在迭代这样的模式,以找到我认为最有效的模式。不确定到底是什么问题。我的直觉表明 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. SwiftUI现在根据枚举值做出决定
            if(self.productViewModel.state == NetworkingModelViewState.hasData) {
                URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
            }
            else {
                Image("loading")
            }

Musings ~ 很难调试声明式框架。它们很强大,我们应该继续学习它们,但要注意卡住。SwiftUI 也让我不得不真正考虑 MVVM。我的收获是,您确实需要分离出控制您的 UI 的所有可能变量。您不应该依赖于读取变量之外的检查。组合未来模式存在内存泄漏,Apple 将在下一个版本中修复。此外,您将能够在 SwiftUI 下一个版本中进行切换。


推荐阅读