Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to refresh view with fetched data - Firestore & SwiftUI

Short: The Images in my view are not updating after the first load. The URL remains the same as the previous loaded view, however the rest of the view that doesn't fetch a URL or data from storage is updated.

Full: I have two Views, a ListView and a DetailView.

In the ListView I display a list of type List. The detail view is supposed to show each Profile from List.profiles. I do this by storing each string uid in List.profiles and calling model.fetchProfiles to fetch the profiles for each list selected.

On the first selected List model.fetchProfiles returns the documents and model.profiles displays the data fine in the DetailView.

When first loading the DetailView the ProfileRow on appear is called and logs the profiles fetched. Then the ProfileRow loads the imageURL from the imagePath and uses it like to fetch the image.

Console: Load List1

CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles [] CARD ROW
CARD ROW DID APPEAR: Profiles profiles/XXXXXX/Profile/profile.png
CARD ROW DID APPEAR: SortedProfiles profiles/XXXXXX/Profile/profile.png
Get url from image path: profiles/XXXXXX/Profile/profile.png
Image URL: https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX

When selecting the second List from ListView the ProfileRow didAppear is not called due to;

if model.profiles.count > 0 {
  print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")     
  print("CARD ROW DID APPEAR: Sorted  \(model.sortedProfiles[0].imgPath)")
}

and won't ever again when selecting a List in ListView, however the rest of the profile data in the ProfileRow is displayed such as name so the data must be fetched.

The ImagePath is the same as the first view loading the exact same image. All other properties for the Profile such as name are loaded correctly.

Console: Load List2

CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles [] CARD ROW
Get url from image path: profiles/XXXXXX/Profile/profile.png Image URL: https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX

If I then navigate to List1 then the image for List2 appears, if I reselect List2 the image appears fine. The image show is correct on first load, and when selecting another list it always the one from before.

Can anyone help me out ?

First View

struct ListViw: View {
  @EnvironmentObject var model: Model

  var body: some View {
    VStack {
        
        ForEach(model.lists.indices, id: \.self) { index in
            NavigationLink(
                destination: DetailView()
                            .environmentObject(model)
                            .onAppear() {
                                model.fetchProfiles()
                            }
            ) {
                 ListRow(home:model.lists[index])
                    .environmentObject(model)
            }
            .isDetailLink(false)
        }
    }
  }

}

DetailView Card

struct ProfilesCard: View {

@EnvironmentObject var model: Model

var body: some View {
    VStack(alignment: .trailing, spacing: 16) {                
            if !model.sortedProfiles.isEmpty {
                VStack(alignment: .leading, spacing: 16) {
                    ForEach(model.sortedProfiles.indices, id: \.self) { index in
                        ProfileRow(
                            name: "\(model.sortedProfiles[index].firstName) \(model.sortedProfiles[index].lastName)",
                            imgPath: model.sortedProfiles[index].imgPath,
                            index: index)
                            .environmentObject(model)
                    }
                }
                .padding(.top, 16)
            }
        
    }//End of Card
    .modifier(Card())
    .onAppear() {
        print("CARD DID APPEAR: Profiles \(model.profiles)")
        print("CARD DID APPEAR: SORTED \(model.sortedTenants)")
    }
}
}



struct ProfileRow: View {

@EnvironmentObject var model: Model

@State var imageURL = URL(string: "")

var name: String
var imgPath: String
var index: Int

private func loadImage() {
    print("load image: \(imgPath)")
    DispatchQueue.main.async {
        fm.getURLFromFirestore(path: imgPath,  success: { (imgURL) in
                   print("Image URL: \(imgURL)")
            imageURL = imgURL
               }) { (error) in
                   print(error)
        }
    }
}

var body: some View {
    VStack(alignment: .leading, spacing: 12) {
        HStack(alignment: .center, spacing: 12) {

                   KFImage(imageURL,options: [.transition(.fade(0.2)), .forceRefresh])
                       .placeholder {
                           Rectangle().foregroundColor(.gray)
                       }
                       .resizable()
                       .aspectRatio(contentMode: .fill)
                       .frame(width: 32, height: 32)
                       .cornerRadius(16)


                // Profile text is always displayed correctly
            Text(name)
                    .modifier(BodyText())
                    .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
    .onAppear() {
        print("CARD ROW")

        // Crashes if check is not there
        if model.profiles.count > 0 {

            print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")

            print("CARD ROW DID APPEAR: Sorted  \(model.sortedProfiles[0].imgPath)")
        }
        
       loadImage()
    }
  }
}

Model

class Model: ObservableObject {

  init() {
      fetchData()
  }

  @Published var profiles: [Profile] = []
  var sortedProfiles: [Profile] {return profiles.removeDuplicates } 

  @Published var list: List? {
      didSet {
          fetchProfiles()
      }
  }

  func fetchData() {
    if let currentUser = Auth.auth().currentUser {
        
        email = currentUser.email!

            db.collection("lists")
                .whereField("createdBy", isEqualTo: currentUser.uid)
                .addSnapshotListener { (querySnapshot, error) in
                guard let documents = querySnapshot?.documents else {
                    return
                }

                self.lists = documents.compactMap { queryDocumentSnapshot -> List? in
                    return try? queryDocumentSnapshot.data(as: List.self)
                }
            }

    }
 }

  func fetchProfiles() {
    profiles.removeAll()
    
    for p in list!.profiles {
        firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
            profiles.append(profile)
        })
    }
  }

}

Update

What I have tried so far is to use didSet for the ImgPath or ImgURL but still not luck. Also have tried using model.profiles directly.

like image 363
RileyDev Avatar asked Feb 11 '21 21:02

RileyDev


People also ask

How do I update my firestore data?

Firestore Update Entire DocumentgetDatabase() → where we want to update a document. doc() → where we'll be passing references of database, collection and ID of a document that we want to update. setDoc() → where we actually pass new data that we want to replace along with the doc() method.

How do I listen to changes on firestore?

You can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.

What is QuerySnapshot in firebase?

A QuerySnapshot contains zero or more QueryDocumentSnapshot objects representing the results of a query. The documents can be accessed as an array via the docs property or enumerated using the forEach method. The number of documents can be determined via the empty and size properties.


1 Answers

In all callbacks with Firestore API make assignment for published or state properties on main queue, because callback might be called on background queue.

So, assuming data is returned and parsed correctly, here is as it should look like

for p in list!.profiles {
    firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
        DispatchQueue.main.async {
           profiles.append(profile)
        }
    })
}

also I would recommend to avoid same naming for your custom types with SDK types - there might be very confusing non-obvious errors

// List model below might conflict with SwiftUI List
return try? queryDocumentSnapshot.data(as: List.self)
like image 91
Asperi Avatar answered Sep 25 '22 19:09

Asperi