0

TLDR is it possible to update a single published property in a ViewModel without causing the entire View that observes it to reload?

I have a Lottie animation on a subview, and am using Firebase Realtime Database to observe changes from a cloud object. When using MVVM, anytime a property receives a change from Firebase the entire view re-loads, causing my animation to start again. I need to maintain an observer of Firebase as other UI changes require a dynamic update, however when any one of them change I don't want to re-draw the entire view (and subsequently set off my animation).

I've tried both @EnvironmentObject and @StateObject (simplified version seen here), as well as using != as a way of preventing an update to the local data from Firebase unless it's different. I've read about Equatable (link), however not sure if that's applicable here? Is it possible to update individual properties and have the view respond specifically to them only? Thanks!

// SomeViewModel() gets created in the App struct

// Main View
struct ContentView: View {
  @StateObject var someViewModel: SomeViewModel

  var body: some View {
   LottieView(someViewModel: someViewModel)
  }
}

// Subview
struct LottieView: View {
  @StateObject var someViewModel: SomeViewModel
    
  var body: some View {
    Lottie(name: someViewModel.lottieToShow, loopMode: .playOnce)
      .frame(width: 250, height: 250)
  }
}

// ViewModel
class SomeViewModel: ObservableObject {
  @Published var lottieToShow: String = "lottie_city_animation"
  @Published var someOtherProperty: Bool = false

  private var isCity: Bool = false { didSet {
    updateLottie()
  }}

  private func updateLottie() {
     if isCity == true {
         self.lottieToShow = "lottie_city_animation"
     } else {
         self.lottieToShow = "lottie_park_animation"
     }
  }

  // Firebase

  private func readObject() {
     ref.child("users").child("01")
        .observe(.value) { snapshot in
           do {
              let firebaseObject = try snapshot.data(as: Object.self)
               if self.isCity != firebaseObject.isCity {
                    self.isCity = firebaseObject.isCity
               }
               if self.someOtherProperty != firebaseObject.someOtherProperty {
                    self.someOtherProperty = firebaseObject.someOtherProperty
               }
           } catch {
               print("Error reading value")
               }
            }
    }
}
moosgrn
  • 379
  • 3
  • 13

1 Answers1

1

It looks like some pieces of code may have been oversimplified, CallerViewModel isn't used anywhere (unless it is presumably the same as SomeViewModel). @Published properties within an ObservableObject all trigger the same publisher to fire, namely the objectWillChange publisher.

So that will be a dead end. In general be wary of ObservableObjects getting too big. You can refactor them apart, or pass in a Publisher to the view and use onReceive(_:perform:) to respond to changes. Or pass certain data dependencies directly to a subview to try and keep changes targeted

nteissler
  • 1,513
  • 15
  • 16
  • Thanks for the comment (I have corrected the class name). I see what you mean now by having the `ObservableObject` getting too big. Since all of the properties come from the Firebase call, how would I refactor the ViewModel apart? – moosgrn Oct 16 '22 at 23:53
  • I ended up using `Equatable` [(Stack Overflow example)](https://stackoverflow.com/questions/60482098/swiftui-how-to-prevent-view-to-reload-whole-body) and setting a `var` in the view that gets checked. – moosgrn Oct 18 '22 at 16:31