Wait For Firebase to Load Before Returning from a Function

Wait for Firebase to load before returning from a function

(Variations on this question come up constantly on SO. I can never find a good, comprehensive answer, so below is an attempt to provide such an answer)

You can't do that. Firebase is asynchronous. Its functions take a completion handler and return immediately. You need to rewrite your loadFromFirebase function to take a completion handler.

I have a sample project on Github called Async_demo (link) that is a working (Swift 3) app illustrating this technique.

The key part of that is the function downloadFileAtURL, which takes a completion handler and does an async download:

typealias DataClosure = (Data?, Error?) -> Void

/**
This class is a trivial example of a class that handles async processing. It offers a single function, `downloadFileAtURL()`
*/
class DownloadManager: NSObject {

static var downloadManager = DownloadManager()

private lazy var session: URLSession = {
return URLSession.shared
}()

/**
This function demonstrates handling an async task.
- Parameter url The url to download
- Parameter completion: A completion handler to execute once the download is finished
*/

func downloadFileAtURL(_ url: URL, completion: @escaping DataClosure) {

//We create a URLRequest that does not allow caching so you can see the download take place
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
let dataTask = URLSession.shared.dataTask(with: request) {
//------------------------------------------
//This is the completion handler, which runs LATER,
//after downloadFileAtURL has returned.
data, response, error in

//Perform the completion handler on the main thread
DispatchQueue.main.async() {
//Call the copmletion handler that was passed to us
completion(data, error)
}
//------------------------------------------
}
dataTask.resume()

//When we get here the data task will NOT have completed yet!
}
}

The code above uses Apple's URLSession class to download data from a remote server asynchronously. When you create a dataTask, you pass in a completion handler that gets invoked when the data task has completed (or failed.) Beware, though: Your completion handler gets invoked on a background thread.

That's good, because if you need to do time-consuming processing like parsing large JSON or XML structures, you can do it in the completion handler without causing your app's UI to freeze. However, as a result you can't do UI calls in the data task completion handler without sending those UI calls to the main thread. The code above invokes the entire completion handler on the main thread, using a call to DispatchQueue.main.async() {}.

Back to the OP's code:

I find that a function with a closure as a parameter is hard to read, so I usually define the closure as a typealias.

Reworking the code from @Raghav7890's answer to use a typealias:

typealias SongArrayClosure = (Array<Song>?) -> Void

func loadFromFireBase(completionHandler: @escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data

if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
})
}

I haven't used Firebase in a long time (and then only modified somebody else's Firebase project), so I don't remember if it invokes it's completion handlers on the main thread or on a background thread. If it invokes completion handlers on a background thread then you may want to wrap the call to your completion handler in a GCD call to the main thread.


Edit:

Based on the answers to this SO question, it sounds like Firebase does it's networking calls on a background thread but invokes it's listeners on the main thread.

In that case you can ignore the code below for Firebase, but for those reading this thread for help with other sorts of async code, here's how you would rewrite the code to invoke the completion handler on the main thread:

typealias SongArrayClosure = (Array<Song>?) -> Void

func loadFromFireBase(completionHandler:@escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data

//Pass songArray to the completion handler on the main thread.
DispatchQueue.main.async() {
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
}
})
}

Wait for firebase fetch to finish before calling another function

Not gonna lie, the way you're doing it is a bit weird, but whatever. You have to await your function calls. You should look at the MDN docs on async/await

getLngLat = async () => {
driverId = firebase.auth().currentUser.uid;

// IMPORTANT: This async call must be awaited
await firebase
.database()
.ref("Ride_Request/" + driverId)
.once("value")
.then((snapshot) => {
if (snapshot.exists()) {
DriverHomeContents.RiderPickUpLatitude = snapshot
.child("pickupLatitude")
.val();
DriverHomeContents.RiderPickUpLongitude = snapshot
.child("pickupLongitude")
.val();
DriverHomeContents.RiderDropUpLatitude = snapshot
.child("dropOffLatitude")
.val();
DriverHomeContents.RiderDropUpLongitude = snapshot
.child("dropOffLongitude")
.val();
);
} else {
this.toast.show("No ride requests", 500);
}
})
}

async componentDidMount() {
// You have to await this first
// this.getRiderRequestDetails(); //first function
// You should do something like this
await this.getLngLat()

this.getDistance(DriverHomeContents.RiderPickUpLatitude, //second function
DriverHomeContents.RiderPickUpLongitude,
DriverHomeContents.RiderDropUpLatitude,
DriverHomeContents.RiderPickUpLongitude)
}


How to wait for Firebase data to be fetched before progressing?

So I managed to fix it somehow. Thanks to Julian for the help

What I did was create an array of promises which will be executed whenever the data changes. The code is:

import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Dimensions, Text, View} from 'react-native';
import firestore from '@react-native-firebase/firestore';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import FolloweringScreens from './FolloweringScreens';
import {TouchableOpacity} from 'react-native-gesture-handler';

const {width, height} = Dimensions.get('screen');

function Following({urlname, navigation}) {
const [followingData, setfollowingData] = useState();
const [loading, setLoading] = useState(true);

// Following counts, displayname, image
const fetchData = useCallback(() => {
const dataRef = firestore().collection('usernames');

dataRef
.doc(urlname)
.collection('Following')
.limit(25)
.onSnapshot((snapshot) => {
let promises = [];
snapshot.forEach((doc) => {
const promise = dataRef
.doc(doc.id.toLowerCase())
.get()
.then((followerDoc) => {
const data = followerDoc.data();

return {
profileName: doc.id,
displayName: data.displayName
? data.displayName
: data.userName,
followerCount:
data.followers !== undefined ? data.followers : 0,
followingCount:
data.following !== undefined ? data.following : 0,
image: data.imageUrl ? data.imageUrl : null,
};
});
promises.push(promise);
});
Promise.all(promises)
.then((res) => setfollowingData(res))
.then(setLoading(false));
});
}, []);

useEffect(() => {
const dataRef = firestore().collection('usernames');

const cleanup = dataRef
.doc(urlname)
.collection('Following')
.limit(25)
.onSnapshot(fetchData);

return cleanup;

// fetchData();
}, [urlname, fetchData]);

return (
<>
<View
style={styles}>
<TouchableOpacity onPress={() => navigation.openDrawer()}>
<Icon name="menu" color="#222" size={30} />
</TouchableOpacity>
<Text style={{left: width * 0.05}}>Following</Text>
</View>

{loading ? (
<ActivityIndicator size="large" color="black" />
) : (
<>
<FolloweringScreens data={followingData} />
</>
)}
</>
);
}

export default Following;

How can I wait until Firebase Database data is retrieved in Flutter?

A listener is something that waits for changes (new documents added etc.) and notifies you whenever that happens. So it isn't something that starts/ends after you call it - you typically create it in the init of your Flutter class, and it changes state whenever something changes.

It looks like you want to grab all the users, once, and not listen like you're currently doing.

So instead of listening, perhaps just read the data. Something like:

// fetch all the documents
final allDocs =
await FirebaseFirestore.instance.collection('users').get();
// map them to something like your strings.
final thedetails = allDocs.docs.map((DocumentSnapshot e) => e.data()!.toString()).toList();
return thedetails;

How do I wait for a Firebase function to complete before continuing my code flow in SwiftUI?

Most of Firebase's API calls are asynchronous, which is why you need to either register a state listener or use callbacks.

Two side notes:

  1. You should not implement ObservableObjects as singletons. Use @StateObject instead, to make sure SwiftUI can properly manage its state.
  2. You no longer need to use PassthroughSubject directly. It's easier to use the @Published property wrapper instead.

That being said, here are a couple of code snippets that show how you can implement Email/Password Authentication with SwiftUI:

Main View

The main view shows if you're sign in. If you're not signed in, it will display a button that will open a separate sign in screen.

import SwiftUI

struct ContentView: View {
@StateObject var viewModel = ContentViewModel()

var body: some View {
VStack {
Text(" Hello!")
.font(.title3)

switch viewModel.isSignedIn {
case true:
VStack {
Text("You're signed in.")
Button("Tap here to sign out") {
viewModel.signOut()
}
}
default:
VStack {
Text("It looks like you're not signed in.")
Button("Tap here to sign in") {
viewModel.signIn()
}
}
}
}
.sheet(isPresented: $viewModel.isShowingLogInView) {
SignInView()
}
}
}

The main view's view model listens for any auth state changes and updates the isSignedIn property accordingly. This drives the ContentView and what it displays.

import Foundation
import Firebase

class ContentViewModel: ObservableObject {
@Published var isSignedIn = false
@Published var isShowingLogInView = false

init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.isSignedIn = true
}
else {
self.isSignedIn = false
}
}
}

/// Show the sign in screen
func signIn() {
isShowingLogInView = true
}

/// Sign the user out
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print("Error while trying to sign out: \(error)")
}
}
}

SignIn View

The SignInView shows a simple email/password form with a button. The interesting thing to note here is that it listens for any changes to the viewModel.isSignedIn property, and calls the dismiss action (which it pulls from the environment). Another option would be to implement a callback as a trailing closure on the view model's signIn() method.

struct SignInView: View {
@Environment(\.dismiss) var dismiss
@StateObject var viewModel = SignInViewModel()

var body: some View {
VStack {
Text("Hi!")
.font(.largeTitle)
Text("Please sign in.")
.font(.title3)
Group {
TextField("Email", text: $viewModel.email)
.disableAutocorrection(true)
.autocapitalization(.none)
SecureField("Password", text: $viewModel.password)
}
.padding()
.background(Color(UIColor.systemFill))
.cornerRadius(8.0)
.padding(.bottom, 8)

Button("Sign in") {
viewModel.signIn()
}
.foregroundColor(Color(UIColor.systemGray6))
.padding(.vertical, 16)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(8)
}
.padding()
.onChange(of: viewModel.isSignedIn) { signedIn in
dismiss()
}
}
}

The SignInViewModel has a method signIn that performs the actual sign in process by calling Auth.auth().signIn(withEmail:password:). As you can see, it will change the view model's isSignedIn property to true if the user was authenticated.

import Foundation
import FirebaseAuth

class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""

@Published var isSignedIn: Bool = false

func signIn() {
Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in
if let error = error {
print("There was an issue when trying to sign in: \(error)")
return
}

guard let user = authDataResult?.user else {
print("No user")
return
}

print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
}
}

Alternative: Using Combine

import Foundation
import FirebaseAuth
import FirebaseAuthCombineSwift

class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""

@Published var isSignedIn: Bool = false

// ...

func signIn() {
Auth.auth().signIn(withEmail: email, password: password)
.map { $0.user }
.replaceError(with: nil)
.print("User signed in")
.map { $0 != nil }
.assign(to: &$isSignedIn)
}
}

Alternative: Using async/await

import Foundation
import FirebaseAuth

class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""

@Published var isSignedIn: Bool = false


@MainActor
func signIn() async {
do {
let authDataResult = try 3 await 1 Auth.auth().signIn(withEmail: email, password: password)
let user = authDataResult.user

print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
catch {
print("There was an issue when trying to sign in: \(error)")
self.errorMessage = error.localizedDescription
}
}
}

More details

I wrote an article about this in which I explain the individual techniques in more detail: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await. If you'd rather watch a video, I've got you covered as well: 3 easy tips for calling async APIs

How to wait for Firebase Task to complete to get result as an await function

Data is loaded from Firebase asynchronously, since it may have to come from the server. While the data is being loaded, your main code continues to run. Then when the data is available, the task completes and your onSuccess gets called.

It's easiest to see what this means in practice by running in a debugger, or by adding some logging:

DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
Log.i("Firebase", "1. Starting to load data");
Task<DataSnapshot> dataSnapshotTask = mDatabase.get();
for (DataSnapshot parking: parkingsData) {
Log.i("Firebase", "2. Got data");
}
Log.i("Firebase", "3. After starting to load data");

When you run this code, it prints:

  1. Starting to load data

  2. After starting to load data

  3. Got data

This is probably not the order that you expected, but it actually working as intended. It also explains why you're not getting a result from your getParkingLots function: by the time your return parkings runs, the parkings.add(parking.getValue(Parking.class)) hasn't been called yet!


The solution for this is always the same: any code that needs the data from the database, needs to be inside your onSuccess method or be called from there. This means you can't return the parking lots from the function, but you can pass in a callback that takes them as a parameter. Or you could return a Task<HashMap<String, Parking>> pretty similar to what Firebase does got get().

For an example of the callback, see my longer explanation on: getContactsFromFirebase() method return an empty list

How to wait for a Firebase retrieve value and only then exit the function?

Use async/await:

async function checkBuyingCondition(prefix) {
var res = '';

var currentPriceOpenRef = firebase.database()
.ref(`dailyT/currentPriceOpen/${nextDayTrading}`)
.orderByChild('prefix')
.equalTo(prefix);

var snapshot = await currentPriceOpenRef.once('value');

if(snapshot.exists()) {
snapshot.forEach(function(childSnapshot) {
var val = childSnapshot.val();
res = `${val.currentPriceOpen}`;
});
} else {
res = 'NA';
}

return res;
}

Take note that this does not make your function synchronous at all, thus the async keyword at the beginning of your function declaration; it just makes your function look like one.

On the 3rd line inside the function you'll notice the await keyword. This waits for your promise to resolve then returns the result which in your case, is the snapshot from Firebase. You can only use await inside async functions.

More Reading: Javascript Async/Await



Related Topics



Leave a reply



Submit