Published computed properties in SwiftUI model objects

前端 未结 3 1575
南笙
南笙 2021-01-07 03:38

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

class Tallies: Identifiable, ObservableObject {
  let id = UUID()
  @Published va         


        
相关标签:
3条回答
  • 2021-01-07 03:42

    The simplest & fastest is to use value-type model.

    Here is a simple demo. Tested & worked with 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 }
        }
    }
    
    0 讨论(0)
  • 2021-01-07 03:48

    This is similar to the last option of @New Devs answer, but a little shorter, essentially just passing the objectWillChange notification to the parent object:

    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 Demo:

    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")
          }
        }
      }
    }
    
    0 讨论(0)
  • 2021-01-07 03:56

    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.
    0 讨论(0)
提交回复
热议问题