-1

I'm calling a Firestore query that does come back, but I need to ensure completion before moving on with the rest of the code. So I need a completion handler...but for the life of me I can't seem to code it.

// get user info from db
    func getUser() async {
        self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                for document in querySnapshot!.documents {
                    let userTrust = document.data()["userTrust"] as! String
                    let userGrade = document.data()["userGrade"] as! String
                    let userDisclaimer = document.data()["userDisclaimer"] as! String
                    
                    var row = [String]()
                    row.append(userTrust)
                    row.append(userGrade)
                    row.append(userDisclaimer)
                    
                    self.userArray.append(row)
                    
                    // set google firebase analytics user info
                    self.userTrustInfo = userTrust
                    self.userGradeInfo = userGrade

                }
            }
            
        }
    }

Called by:

internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        FirebaseApp.configure()
        db = Firestore.firestore()
        Database.database().isPersistenceEnabled = true

        Task {
            do {
                let userInfo = await getUser()
            }
        } return true }

I used a Task as didFinishLauncingWithOptions is synchronous and not asynchronous

However, the getUser() still isn't completed before didFinishLauncingWithOptions moves on.

I need the data from getUser as the very next step uses the data in the array, and without it I get an 'out of bounds exception' as the array is still empty.

Also tried using dispatch group within the func getUser(). Again with no joy.

Finally tried a completion handler:

func getUser(completion: @escaping (Bool) -> Void) {

        self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {

                for document in querySnapshot!.documents {

                    let userTrust = document.data()["userTrust"] as! String
                    let userGrade = document.data()["userGrade"] as! String
                    let userDisclaimer = document.data()["userDisclaimer"] as! String
                    
                    var row = [String]()
                    row.append(userTrust)
                    row.append(userGrade)
                    row.append(userDisclaimer)
                    
                    self.userArray.append(row)
                    
                    // set google firebase analytics user info
                    self.userTrustInfo = userTrust
                    self.userGradeInfo = userGrade
                    
                    completion(true)
                    }
                
                }
            }
        
        }

Nothing works. The getUser call isn't completed before the code moves on. Can someone please help. I've searched multiple times, looked at all linked answers but I can not make this work.I'm clearly missing something easy, please help

Nicholas Farmer
  • 773
  • 7
  • 32
  • Does this answer your question? [SwiftUI - Wait until Firestore getDocuments() is finished before moving on](https://stackoverflow.com/questions/68047113/swiftui-wait-until-firestore-getdocuments-is-finished-before-moving-on) – Bradley Mackey Jan 31 '23 at 22:50
  • `Task` and `async` are pointless in your code, the body if `getUser` is asynchronous via completion handlers which needs to be converted to be compatible with `async/await`/the new Concurrency. We have provided links with proper async await code in your previous questions on the same subject. – lorem ipsum Feb 01 '23 at 01:38
  • @BradleyMackey thanks, I’ve seen this and tried to implement without any luck. – Nicholas Farmer Feb 01 '23 at 05:44
  • @loremipsum I don’t understand how to convert my code. Previous question was closed so posted this with updates asked for. I’m at a loss where to go next. Any more specific advice appreciated – Nicholas Farmer Feb 01 '23 at 05:47
  • @NicholasFarmer Ignore the SwiftUI part of that question and it's the same as what you are asking here. The key is that `getDocuments()` is asynchronous and is returning it's data in a completion handler. I'd suggest learning more about asynchronous completion handlers, then Swift Concurrency (Task/async/await), then tackling this problem if you are still unsure. – Bradley Mackey Feb 01 '23 at 09:58
  • @BradleyMackey Thanks for advice. In regards to completion handler, I thought thats what I needed, and tried. But no luck. Besides the link you initially sent, any others (maybe tutorials) that you think for my level would be good to look at? – Nicholas Farmer Feb 01 '23 at 19:05
  • Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. – lorem ipsum Feb 03 '23 at 23:39
  • @loremipsum I’ll try and re do my question later, with code change (attempting to use your initial advice). This is a real sticking point for me, one which I can’t progress without solving. Thank you for persisting with advice – Nicholas Farmer Feb 04 '23 at 07:18
  • I just DON'T understand! I've watched several videos on async/await, read tutorials etc and I just can't get my head around this. I simply want a function call to get documents from Firestone to actually complete (get the data) before continuing on with the next line of code. I simply don't know what to do. Everyone is using examples with very different calls, structs etc. I can't see the wood for the trees. I've been hitting my head against a wall for 2 weeks. Please can someone walk me through this? – Nicholas Farmer Feb 05 '23 at 21:44
  • We help people here all the time but you haven't even provided a full picture. There are other websites like codementor.io where you can get 1:1 help and someone can get a full picture by looking at your code. Trying to make an asynchronous call in `didFinishLaunchingWithOptions` and expect it to be done before the `func` returns is not achievable but there are many ways to account for this and react properly. Ive tried to keep on eye on this to see if you provide an MRE. – lorem ipsum Feb 06 '23 at 00:26
  • I truly felt I've painted as best picture as I could. Problem is experienced coders like yourself maybe forget that us novices can only ask / post what we know or think is relevant. Thank you engaging. I've looking at reworking the app structure / calls so to achieve – Nicholas Farmer Feb 07 '23 at 16:14
  • @NicholasFarmer I'd say stay away from async/await until you fully understand completion handlers. So let's reassess the point of a completion hander: the code "continues" after `getUser` because if it didn't, you'd be left with a "hang" because that thread can't do any work while the network call is taking place. The point of this is so the system can always be doing something useful and never just waiting around. Therefore, the completion handler will be called **after** your `getUser` call finishes, only when the data is ready from the network. – Bradley Mackey Feb 08 '23 at 10:08

2 Answers2

1

read this post: Waiting for data to be loaded on app startup.

It explains why you should never wait for data before returning from function application(_:didFinishLaunchingWithOptions).

To achieve what you need, you could use your first ViewController as a sort of splashscreen (that only shows an image or an activity indicator) and call the function getUser(completion:) in the viewDidLoad() method the ViewController.

Example:

class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        MyFirestoreDatabaseManager.shared.getUser() { success in
            if success {
                //TODO: Navigate to another ViewController
            } else {
                //TODO: Show an error
            }
        }
    }
}

Where obviously MyFirestoreDatabaseManager.shared is the object on which you defined the getUser(completion:) method.

(In your example, I think that you defined that function in the AppDelegate. In that case, you should mark your getUser(completion:) method and all related variables as static. Then replace MyFirestoreDatabaseManager.shared with AppDelegate).

  • Thank you, that is helpful. I wasn't aware of the issue using didlaunchwithoptions (lorem posted a comment stating not possible in this function but I wasn't aware why - now I am). – Nicholas Farmer Feb 07 '23 at 16:10
-1

Not 100% sure what you would like to accomplish as I can't see all your code, but try something similar to this, replacing Objects for what you are trying to return from the documents.

You don't want your user's data spread across multiple documents. With Firebase you pay for every document you have to get. Ideally you want all your user's settings within one firebase document. Then create a UserInfo struct that you can decode to using the library CodeableFirebase or the decoder of your choice.

// Create user struct
struct UserInfo: Codable {
    var userId: String
    var userTrust: String
    var userGrade: String
    var userDisclaimer: String
}

// get user info from db and decode using CodableFirebase
func getUser() async throws -> UserInfo {
    let doc = try await self.db.collection("users").document("userIdHere")
    let userInfo = try FirestoreDecoder().decode(UserInfo.self, from: doc.data())
    return UserInfo
}

Then you can do this...

Task {
    do {
        let userInfo = try await getUser()
        let userTrust = userInfo.userTrust
        let userGrade = userInfo.userGrade
        let userDisclaimer = userInfo.userDisclaimer
    }
} 
anomaddev
  • 9
  • 2
  • Appreciate you help, however this doesn't work I'm afraid. I'm trying to ensure my Firestore query is completed before moving on with the rest of the code – Nicholas Farmer Feb 05 '23 at 20:21
  • Cannot find 'Firestoredecoder' in scope – Nicholas Farmer Feb 05 '23 at 21:54
  • User info is just an example of one query I'm doing. I want to load my various arrays (userSetting is just one) from Firestore upon app initialisation. I chose this one as I need it in order to either have my first viewController go to the app (used before and got disclaimer etc) or first time used view controller (new user, not got disclaimer). I need it to run as the first part of didLaunchWithOptions function. Is there a way of using what you've put without using a struct / decoder? Based around how I'm getting the data from Firestore? – Nicholas Farmer Feb 05 '23 at 22:10
  • You should have all your information in one User document. Putting that information in multiple documents for the same user is not the way to go about this. Good luck. Again, I suggest you take either my advice or the above commenters advice because if you don't your app will not run efficiently in production. – anomaddev Feb 05 '23 at 22:16