【问题标题】:SwiftUI View not updating on async change to published properties of Observed ObjectSwiftUI 视图未在对已观察对象的已发布属性的异步更改时更新
【发布时间】:2020-09-15 15:46:30
【问题描述】:

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

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

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

【问题讨论】:

  • 尝试将 self.product = product 包装到 DispatchQueue.main.async {} developer.apple.com/documentation/dispatch/dispatchqueue/…
  • 感谢您的建议。已经试过了,还是不行
  • 猜测效率低下...你会准备最小可重现的例子吗?
  • 刚刚添加了一些附加信息,以便可以复制。

标签: swiftui combine observedobject


【解决方案1】:

我一直在迭代这样的模式,以找到我认为最有效的模式。不确定到底是什么问题。我的直觉表明 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. 现在根据 Enum 值在您的 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 下一个版本中进行切换。

【讨论】:

  • 谢谢,我认为是这样的。当我在调试器中单步执行代码时,产品视图中的主体仅在 init 之后调用一次。当未来交付其价值时,视图根本不会更新。不过,我有其他观点,我通过 Currentvaluesubject 更新状态并且有效。所以很可能是未来造成了这种情况。如果有其他答案,我会等到明天。如果没有,我会接受你的。
猜你喜欢
  • 2022-01-04
  • 2020-11-26
  • 1970-01-01
  • 2019-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-15
相关资源
最近更新 更多