在SwiftUI模型对象中发布计算属性 [英] Published computed properties in SwiftUI model objects

查看:55
本文介绍了在SwiftUI模型对象中发布计算属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我的SwiftUI应用中有一个数据模型,如下所示:

  class Tallies:可识别的ObservableObject {设id = UUID()@已发布的变量计数= 0}类GroupOfTallies:可识别的ObservableObject {设id = UUID()@已发布的var元素:[Tallies] = []} 

我想向 GroupOfTallies 添加类似于以下内容的计算属性:

 //返回组中所有元素的计数总和var累积计数:整数{返回elements.reduce(0){$ 0 + $ 1.count}} 

但是,我希望SwiftUI在 cumulativeCount 更改时更新视图.当 elements 更改(数组获得或丢失元素)或任何包含的 Tallies 对象的 count 字段更改时,就会发生这种情况.>

我已经考虑将其表示为 AnyPublisher ,但是我认为我对Combine并没有足够的了解以使其正常工作.在此答案中提到过,但是从中创建的AnyPublisher基于发布的 Double ,而不是发布的 Array .如果我尝试使用相同的方法而不进行修改,则 cumulativeCount 仅在elements数组更改时更新,而在其中一个元素的 count 属性更改时不更新.

解决方案

这里有多个问题要解决.

首先,重要的是要了解,SwiftUI在检测到更改时会在 @State 属性中或从 ObservableObject (通过> @ObservedObject @EnvironmentObject 属性包装器).

在后一种情况下,这可以通过 @Published 属性完成,也可以通过 objectWillChange.send()手动完成. objectWillChange 是可在任何 ObservableObject 上使用的 ObservableObjectPublisher 发布者.

这是一个很长的说法,即 IF 计算属性中的更改是一起与任何的更改一起引起的@Published 属性-例如,从某处添加另一个元素时:

  elements.append(Talies()) 

然后无需执行其他任何操作-SwiftUI将重新计算观察到的视图,并读取计算出的属性 cumulativeCount 的新值.


当然,如果 Tallies 对象之一的 .count 属性发生更改,则不会导致 elements 发生更改,因为 Tallies 是引用类型.

给出简化示例的最佳方法实际上是使其成为值类型-一种 struct :

  struct提示:可识别的{设id = UUID()var count = 0} 

现在,任何 Tallies 对象的更改都将导致 elements 的更改,这将导致观察到"视图.它可以获取计算属性的最新值.同样,不需要额外的工作.


但是,如果您坚持认为 Tallies 由于某种原因不能成为值类型,那么您需要通过订阅以下内容来监听 Tallies 中的任何更改:他们的 .objectWillChange 发布者:

  GroupOfTallies类:可识别的ObservableObject {设id = UUID()@已发布的var元素:[Tallies] = [] {didSet {cancellables = []//取消上一个订阅elements.publisher.flatMap {$ 0.objectWillChange}.sink(receiveValue:self.objectWillChange.send).store(位于:& cancellables)}}private var cancellables = Set< AnyCancellable>var累积计数:整数{return elements.reduce(0){$ 0 + $ 1.count}//此处无更改}} 

以上内容将通过以下方式订阅 elements 数组中的更改(以考虑添加和删除):

  • 将数组转换为每个数组元素的 Sequence 发布者
  • 然后再将flatMap的每个数组元素(是对象)放入其 objectWillChange 发布者
  • 然后对于任何输出,调用 objectWillChange.send(),以通知观察其自身的视图.

Suppose I have a data model in my SwiftUI app that looks like the following:

class Tallies: Identifiable, ObservableObject {
  let id = UUID()
  @Published var count = 0
}

class GroupOfTallies: Identifiable, ObservableObject {
  let id = UUID()
  @Published var elements: [Tallies] = []
}

I want to add a computed property to GroupOfTallies that resembles the following:

// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
  return elements.reduce(0) { $0 + $1.count }
}

However, I want SwiftUI to update views when the cumulativeCount changes. This would occur either when elements changes (the array gains or loses elements) or when the count field of any contained Tallies object changes.

I have looked into representing this as an AnyPublisher, but I don't think I have a good enough grasp on Combine to make it work properly. This was mentioned in this answer, but the AnyPublisher created from it is based on a published Double rather than a published Array. If I try to use the same approach without modification, cumulativeCount only updates when the elements array changes, but not when the count property of one of the elements changes.

解决方案

There are multiple issues here to address.

First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a @State property, or from an ObservableObject (via @ObservedObject and @EnvironmentObject property wrappers).

In the latter case, this is done either via a @Published property, or manually with objectWillChange.send(). objectWillChange is an ObservableObjectPublisher publisher available on any ObservableObject.

This is a long way of saying that IF the change in a computed property is caused together with a change of any @Published property - for example, when another element is added from somewhere:

elements.append(Talies())

then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount.


Of course, if the .count property of one of the Tallies objects changes, this would NOT cause a change in elements, because Tallies is a reference-type.

The best approach given your simplified example is actually to make it a value-type - a struct:

struct Tallies: Identifiable {
  let id = UUID()
  var count = 0
}

Now, a change in any of the Tallies objects would cause a change in elements, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.


If you insist, however, that Tallies cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies by subscribing to their .objectWillChange publishers:

class GroupOfTallies: Identifiable, ObservableObject {
   let id = UUID()
   @Published var elements: [Tallies] = [] {
      didSet {
         cancellables = [] // cancel the previous subscription
         elements.publisher
            .flatMap { $0.objectWillChange }
            .sink(receiveValue: self.objectWillChange.send) 
            .store(in: &cancellables)
      }
   }

   private var cancellables = Set<AnyCancellable>

   var cumulativeCount: Int {
     return elements.reduce(0) { $0 + $1.count } // no changes here
   }
} 

The above will subscribe a change in the elements array (to account for additions and removals) by:

  • converting the array into a Sequence publisher of each array element
  • then flatMap again each array element, which is a Tallies object, into its objectWillChange publisher
  • then for any output, call objectWillChange.send(), to notify of the view that observes it of its own changes.

这篇关于在SwiftUI模型对象中发布计算属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆