在SwiftUI模型对象中发布计算属性 [英] Published computed properties in SwiftUI model objects
问题描述
假设我的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 itsobjectWillChange
publisher - then for any output, call
objectWillChange.send()
, to notify of the view that observes it of its own changes.
这篇关于在SwiftUI模型对象中发布计算属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!