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:
- You should not implement
ObservableObjects
as singletons. Use@StateObject
instead, to make sure SwiftUI can properly manage its state. - 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:
Starting to load data
After starting to load data
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
Update Badge With Push Notification While App in Background
Objective-C and Swift Url Encoding
How to Add Uitableview Within a Uitableviewcell
Starting Iphone App Development in Linux
How to Apply Gradient to Background View of iOS Swift App
How to Get Current Location from User in Ios
How to Create a Uicolor from a Hex String
Iphone: Detecting User Inactivity/Idle Time Since Last Screen Touch
How to Convert String Date to Nsdate
Move View With Keyboard Using Swift
Is [Uiscreen Mainscreen].Bounds.Size Becoming Orientation-Dependent in Ios8
How to Use Scnetworkreachability in Swift
Change User Agent in Uiwebview
Iphone 6 Plus Resolution Confusion: Xcode or Apple'S Website? For Development
Airpods Not Working as an Input Source for Voice Recorder App