SwiftUI模型对象中已发布的计算属性

SwiftUI模型对象中已发布的计算属性,swift,swiftui,combine,Swift,Swiftui,Combine,假设我的SwiftUI应用程序中有一个如下所示的数据模型: class Tallies: Identifiable, ObservableObject { let id = UUID() @Published var count = 0 } class GroupOfTallies: Identifiable, ObservableObject { let id = UUID() @Published var elements: [Tallies] = [] } // Ret

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

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

class GroupOfTallies: Identifiable, ObservableObject {
  let id = UUID()
  @Published var elements: [Tallies] = []
}
// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
  return elements.reduce(0) { $0 + $1.count }
}
我想向
groupoftraines
添加一个类似于以下内容的计算属性:

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

class GroupOfTallies: Identifiable, ObservableObject {
  let id = UUID()
  @Published var elements: [Tallies] = []
}
// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
  return elements.reduce(0) { $0 + $1.count }
}
但是,我希望SwiftUI在
cumulativeCount
更改时更新视图。当
元素
发生变化(数组获得或丢失元素)或任何包含的
对象的
计数
字段发生变化时,都会发生这种情况


我已经研究过将它表示为一个
AnyPublisher
,但我认为我对Combine的掌握还不够好,无法使它正常工作。中提到了这一点,但从中创建的AnyPublisher是基于已发布的
Double
而不是已发布的
Array
。如果我尝试在不进行修改的情况下使用相同的方法,
cumulativeCount
仅在元素数组更改时更新,但在其中一个元素的
count
属性更改时不会更新。

最简单和最快的方法是使用值类型模型

这里是一个简单的演示。使用Xcode 12/iOS 14进行测试和工作

struct TestTallies: View {
    @StateObject private var group = GroupOfTallies()     // SwiftUI 2.0
//    @ObservedObject private var group = GroupOfTallies()     // SwiftUI 1.0

    var body: some View {
        VStack {
            Text("Cumulative: \(group.cumulativeCount)")
            Divider()
            Button("Add") { group.elements.append(Tallies(count: 1)) }
            Button("Update") { group.elements[0].count = 5 }
        }
    }
}

struct Tallies: Identifiable {       // << make struct !!
  let id = UUID()
  var count = 0
}

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

    var cumulativeCount: Int {
      return elements.reduce(0) { $0 + $1.count }
    }
}
结构测试计数:查看{ @StateObject private var group=groupoftraines()//SwiftUI 2.0 //@ObservedObject private var group=groupoftraines()//SwiftUI 1.0 var body:一些观点{ VStack{ 文本(“累计:\(group.cumulativeCount)”) 分隔器() 按钮(“添加”){group.elements.append(计数(计数:1))} 按钮(“更新”){group.elements[0].count=5} } } }
结构计数:可识别{/这里有多个问题需要解决

首先,重要的是要了解SwiftUI在检测到更改时会更新视图的主体,无论是在
@State
属性中还是从
ObserveObject
(通过
@ObserveObject
@EnvironmentObject
属性包装器)

在后一种情况下,这可以通过
@Published
属性完成,也可以使用
objectWillChange手动完成。send()
objectWillChange
是一个
可观测对象发布者
发布者,可在任何
可观测对象
上使用

如果计算属性中的更改与任何已发布的
@属性的更改一起导致,例如,当从某处添加另一个元素时,这是一个很长的说法

elements.append(Talies())
则无需执行任何其他操作-SwiftUI将重新计算观察到它的视图,并读取计算属性的新值
cumulativeCount


当然,如果其中一个
TALLES
对象的
.count
属性发生更改,这不会导致
元素的更改,因为
TALLES
是一种引用类型

给出简化示例的最佳方法实际上是将其设置为值类型-a
struct

struct Tallies: Identifiable {
  let id = UUID()
  var count = 0
}
现在,任何
计数
对象的更改都会导致
元素的更改
,这将导致“观察”它的视图获得计算属性的新值。同样,不需要额外的工作


但是,如果您坚持认为无论出于何种原因,
计数
都不能是值类型,那么您需要通过订阅其
来收听
计数
中的任何更改。objectWillChange
发布者:

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
   }
} 
class-groupoftraines:可识别、可观察的对象{
设id=UUID()
@已发布的变量元素:[计数]=[]{
迪塞特{
Cancelables=[]//取消以前的订阅
elements.publisher
.flatMap{$0.objectWillChange}
.sink(接收值:self.objectWillChange.send)
.store(在:&可取消项中)
}
}
private var cancelables=Set
var累积数:Int{
返回元素。reduce(0){$0+$1.count}//此处无更改
}
} 
上述内容将通过以下方式订阅
元素
数组中的更改(以说明添加和删除):

  • 将数组转换为每个数组元素的
    序列
  • 然后,将每个数组元素(即
    计数的
    对象)再次平面映射到其
    对象将更改
    发布者
  • 然后,对于任何输出,调用
    objectWillChange.send()
    ,通知观察它自身更改的视图

这与
@New Dev
的答案的最后一个选项类似,但稍微短一点,本质上只是将
objectWillChange
通知传递给父对象:

import Combine

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

  func increase() {
    count += 1
  }
}

class GroupOfTallies: Identifiable, ObservableObject {
  let id = UUID()
  var sinks: [AnyCancellable] = []
  
  @Published var elements: [Tallies] = [] {
    didSet {
      sinks = elements.map {
        $0.objectWillChange.sink( receiveValue: objectWillChange.send)
      }
    }
  }

  var cumulativeCount: Int {
    return elements.reduce(0) { $0 + $1.count }
  }
}
SwiftUI演示:

struct ContentView: View {
  @ObservedObject
  var group: GroupOfTallies

  init() {
    let group = GroupOfTallies()
    group.elements.append(contentsOf: [Tallies(), Tallies()])
    self.group = group
  }

  var body: some View {
    VStack(spacing: 50) {
      Text( "\(group.cumulativeCount)")
      Button( action: group.elements.first!.increase) {
        Text( "Increase first")
      }
      Button( action: group.elements.last!.increase) {
        Text( "Increase last")
      }
    }
  }
}

这是一个问题,但你也需要接收你以后添加的元素。@CasperZandbergen是你对最后一种方法的评论吗?你说的接收你以后添加的元素是什么意思?如果有什么问题的话,问题是每次添加都会复制发布者。我想我已经修复了它。最后一种方法不是我建议解决的方法OP的问题-就在这里,我使用的数据存储依赖于使用对象,因此您在最后提到的方法似乎是解决此问题的最佳方法。尽管解决方案有一个保留周期,
groupoftraines
将永远不会发布,除非手动将
elements
设置为
[]
.sink
闭包需要一个
[弱自我]
注释。