【问题标题】:Published computed properties in SwiftUI model objects在 SwiftUI 模型对象中发布的计算属性
【发布时间】:2023-10-20 22:23:01
【问题描述】:

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

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

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

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

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

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

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

【问题讨论】:

    标签: swift swiftui combine


    【解决方案1】:

    这里有多个问题需要解决。

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

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

    这是一个很长的说法,IF计算属性的变化是由任何@Published的变化引起的一起属性 - 例如,当从某处添加另一个元素时:

    elements.append(Talies())
    

    那么就不需要做任何其他事情了 - SwiftUI 将重新计算观察它的视图,并读取计算属性 cumulativeCount 的新值。


    当然,如果Tallies 对象之一的.count 属性发生变化,这不会导致elements 发生变化,因为Tallies 是一个引用类型。

    给定您的简化示例的最佳方法实际上是使其成为值类型 - struct

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

    现在,任何Tallies 对象的更改都会导致elements 的更改,这将导致“观察”它的视图获取计算属性的新值。同样,不需要额外的工作。


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

    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
       }
    } 
    

    以上将订阅elements数组中的更改(以考虑添加和删除):

    • 将数组转换为每个数组元素的 Sequence 发布者
    • 然后再次将每个数组元素(即Tallies 对象)平面映射到其objectWillChange 发布者中
    • 然后对于任何输出,调用objectWillChange.send(),以通知观察它自己的变化的视图。

    【讨论】:

    • 这个,但您还需要接收稍后添加的元素。
    • @CasperZandbergen 您对最后一种方法有何评论?稍后添加的接收器元素是什么意思?如果有的话,问题是每次添加都会重复发布者。我修好了,我想。最终,最后一种方法不是我推荐的解决 OP 问题的方法 - 只是为了完整性
    • 我使用的数据存储依赖于使用对象,所以你最后提到的方法似乎是解决这个问题的最佳方法。
    • 虽然解决方案有一个保留周期,但GroupOfTallies 永远不会被释放,除非手动将elements 设置为[].sink 闭包需要 [weak self] 注释。
    【解决方案2】:

    这类似于@New Devs 答案的最后一个选项,但更短一些,本质上只是将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")
          }
        }
      }
    }
    

    【讨论】:

      【解决方案3】:

      最简单最快的是使用值类型模型。

      这是一个简单的演示。使用 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 }
          }
      }
      

      【讨论】:

        最近更新 更多