1

I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:

// ViewModel.swift

import Foundation
import Firebase
import FirebaseFirestore

class UserViewModel: ObservableObject {
    let current_user_id = Auth.auth().currentUser!.uid
    private var db = Firestore.firestore()
    @Published var xp:Int?
    
    func fetchData(completion: @escaping () -> Void) {
        let docRef = db.collection("users").document(current_user_id)
        
        docRef.getDocument { snapshot, error in
              print(error ?? "No error.")
              self.xp = 0
              guard let snapshot = snapshot else {
                  completion()
                  return
              }
            self.xp = (snapshot.data()!["xp"] as! Int)
            completion()
        }
    }
}
// View.swift

import SwiftUI
import CoreData
import Firebase

{
    @ObservedObject private var users = UserViewModel()
    var body: some View {
        VStack {
            HStack {
                // ...
                Text("xp: \(users.xp ?? 0)")
//                    Text("xp: 1500")
                    .fontWeight(.bold)
                    .padding(.horizontal)
                    .foregroundColor(Color.white)
                    .background(Color("Black"))
                    .clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
                    .padding(.trailing)
            }
            .padding(.top)
            .onAppear() {
                self.users.fetchData()
            }
// ...
    }
}

My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?

trndjc
  • 11,654
  • 3
  • 38
  • 51
Memphis Meng
  • 1,267
  • 2
  • 13
  • 34
  • 1
    You're not showing where `fetchData` is called. Also, are you *sure* that `self.xp` is getting assigned? Have you set break points or print statements to make sure you're getting the value you expect? – jnpdx Apr 23 '21 at 21:20
  • It's also not really clear what the completion handler is supposed to be doing in this case. Your view will automatically respond to the @Published property, further reinforcing the idea that the code probably is not actually successfully reaching or assigning `self.xp` – jnpdx Apr 23 '21 at 21:37
  • Well, I just believe because Firebase always retrieves data asynchronously, completion() handler is helpful to indicate that this step needs to be done before any other. Do u mean that completion() is useless here? – Memphis Meng Apr 23 '21 at 21:56
  • You call site that you've added in your edit doesn't even seem to be valid (it doesn't provide an argument for the completion handler. So, yes, for that reason and what I mentioned above, it does seem to be useless here. – jnpdx Apr 23 '21 at 22:00

1 Answers1

1

I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.

That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.

class UserViewModel: ObservableObject {
    @Published var xp: Int?
    
    init() {
        guard let uid = Auth.auth().currentUser?.uid else {
            return
        }
        let docRef = Firestore.firestore().collection("users").document(uid)

        docRef.getDocument { (snapshot, error) in
            if let doc = snapshot,
               let xp = doc.get("xp") as? Int {
                self.xp = xp
            } else if let error = error {
                print(error)
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var users = UserViewModel()

    var body: some View {
        VStack {
            HStack {
                Text("xp: \(users.xp ?? 0)")
            }
        }
    }
}

SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.

trndjc
  • 11,654
  • 3
  • 38
  • 51
  • Honestly I cannot see any difference between your answer and mine. Not to mention that your declaration using `guard let` is illegal. – Memphis Meng Apr 23 '21 at 22:32
  • Then you have a data problem, a network problem, or an authorization problem so remove code from the list and continue debugging. – trndjc Apr 23 '21 at 22:34
  • Illegal to who? – trndjc Apr 23 '21 at 22:34
  • No, I just meant that when I copied your snippet, it shows `initializer for conditional binding must have Optional type, not 'String'` on the line of `guard let`. – Memphis Meng Apr 23 '21 at 22:36
  • 1
    Make sure `currentUser?` is optionally chained and not force unwrapped like you had it: `guard let uid = Auth.auth().currentUser?.uid` – trndjc Apr 23 '21 at 22:36
  • Brilliant, that is a good catch! But it still is displaying 0 in the `Text` now. Still stuck in the async stuff... :( What is your reason to give up using completion() handler? – Memphis Meng Apr 23 '21 at 22:41
  • I edited the code with print statements. What prints? – trndjc Apr 23 '21 at 22:44
  • The problem then is the call to fetch data in `onAppear` on the view element which can cause unwanted behaviors. If you simply make the network call in the view model's initializer, the problem is solved (edits made). SwiftUI is not my favorite framework and this is one reason why--it's very new and still being developed and this kind of behavior is a side effect. – trndjc Apr 23 '21 at 23:08
  • 1
    Read the most-upvoted comment in the accepted answer and you will see where you need to solve your problem ultimately, finding the correct place to invoke the call to fetch data (if the initializer is not where you want to do it). https://stackoverflow.com/questions/56496359/swiftui-view-viewdidload – trndjc Apr 23 '21 at 23:10
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231537/discussion-between-memphis-meng-and-bxod). – Memphis Meng Apr 23 '21 at 23:13