1

I've simplified my code down so it's easier to see what i'm trying to do, I am building a feed of ratings that are sourced from firebase documents of whomever I am "following"

All that I want to do is whenever firebase says there is a new update to one of my reviews, change that Identifiable, as apposed to having to re-load every review from every person i'm following.

Here's my view:

import SwiftUI
import Firebase

struct Rating: Identifiable {
    var id: String
    var review: String
    var likes: Int
}

struct Home: View {
    
    @ObservedObject var feedViewModel = FeedViewModel()

    var body: some View {
        VStack{
                         
           ForEach(self.feedViewModel.allRatings, id: \.id){ rating in
               Text(rating.review)
               Text("\(rating.likes)")
           }
        }
     }
}

Here are my functions for FeedViewModel:

class FeedViewModel: ObservableObject {

     @Published var following = ["qedXpEcaRLhIa6zWjfJC", "1nDyDT4bIa7LBEaYGjHG", "4c9ZSPTQm2zZqztNlVUp", "YlvnziMdW8VfibEyCUws"]
     @Published var allRatings = [Rating]()

     init() {
         for doc in following {
              
              // For each person (aka doc) I am following we need to load all of it's ratings and connect the listeners
              getRatings(doc: doc)
              initializeListener(doc: doc)

          }
      }


    func getRatings(doc: String) {
        
        db.collection("ratings").document(doc).collection("public").getDocuments() { (querySnapshot, err) in
            
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                
 
                // We have to use += because we are loading lots of different documents into the allRatings array, allRatings turns into our feed of reviews.
                self.allRatings += querySnapshot!.documents.map { queryDocumentSnapshot -> Rating in
                   let data = queryDocumentSnapshot.data()
                   let id = ("\(doc)-\(queryDocumentSnapshot.documentID)")
                   let review = data["review"] as? String ?? ""
                   let likes = data["likes"] as? Int ?? 0

                   return Rating(id: id, review: review, likes: likes)

                }
            }
      }


    func initializeListener(doc: String){
        
        db.collection("ratings").document(doc).collection("public")
            .addSnapshotListener { (querySnapshot, error) in
            
            guard let snapshot = querySnapshot else {
                print("Error listening for channel updates")
                return
            }

            snapshot.documentChanges.forEach { change in

                for document in snapshot.documents{

                   let data = document.data()
                   let id = ("\(doc)-\(document.documentID)")
                   let review = data["review"] as? String ?? ""
                   let likes = data["likes"] as? Int ?? 0

                   // I thought swiftui Identifiables would be smart enough to not add another identifiable if there is already an existing one with the same id, but I was wrong, this just duplicates the rating
                   // self.allRatings.append(Rating(id: id, review: review, likes: likes))

                   // This is my current solution, to overwrite the Rating in the allRatings array, however I can not figure out how to get the index of the changed rating
                   // "0" should be replaced with whatever Rating index is being changed
                   self.allRatings[0] = Rating(id: id, review: review, likes: likes)

                }
            }
        }
     }

}

All I want is to make the "likes" of each rating live and update whenever someone likes a rating. It seems simple but I am relatively new to swiftui so I might be completely off on how I am doing this. Any help is greatly appreciated!

joshorrom
  • 21
  • 4
  • 1
    Add ObservableObject and @Published to Rating and its properties. That way when a Rating updates one of its published-properties the View will automatically redraw. – Helperbug May 16 '21 at 22:09
  • @Helperbug thank you for such a quick response! I don't think I fully understand what you're suggesting. Are you recommending that I move my "struct Rating: Identifiable" into my FeedViewModel Observable object class? If so how would I overwrite one of the ratings inside of my allRatings array? – joshorrom May 17 '21 at 02:04
  • Rating needs to be an observable with published properties. When a rating's property is updated it will publish the change and the view will know to update itself. Try and change the rating class to: class Rating: ObservableObject, Identifiable { var id: String @Published var review: String Published var likes: Int } – Helperbug May 17 '21 at 02:43
  • Dang I really felt like this was going to work haha Sadly I am getting the following error: "Argument passed to call that takes no arguments Remove '(id: id, review: review, likes: likes)'" whenever I try to run any function that tries to write / push to the Ratings identifiable. – joshorrom May 17 '21 at 23:02

1 Answers1

1

After a week of trying to figure this out I finally got it, hopefully this helps somebody, if you want to overwrite an identifiable in your array find the index by using your id. In my case my ID is the firebaseCollectionID + firebaseDocumentID, so this makes it always 100% unique AND it allows me to reference it from a snapshot listener...

let index = self.allRatings.firstIndex { (Rating) -> Bool in
    Rating.id == id
}

Then overwrite it by doing:

self.allRatings[index!] = Rating(id: id, review: review, likes: likes)
joshorrom
  • 21
  • 4
  • 1
    Always use caution when force-unwrapping a variable with the ! operator. I'd advise safely unwrapping with either a `guard let index = index else {return}` or `if let index = index { ... }` – TealShift Apr 11 '22 at 19:23