Calling asynchronous Firebase APIs from Swift

Callbacks, Combine, and async/await


Jan 31, 2022 • 9 min read

Most of Firebase’s APIs are asynchronous.

This might be confusing at first: you’re making a call, for example to fetch some data from Firestore, but you don’t get a result back.

Why does this happen, what does “asynchronous API” mean, and how do you even call asynchronous APIs in Swift?

If you’ve been asking yourself these questions, you’re not alone - quite literally, these are some of the most frequently asked questions about Firebase on StackOverflow and on our GitHub repos.

In this post, I am going to explain what this all means, and show you three easy ways to call Firebase’s APIs asynchronously from your Swift / SwiftUI app.

“Can I please get a skinny latte, extra-hot”

To understand the nature of asynchronous APIs, let’s imagine we’re at a coffee bar, and you’ve just placed your order. The barista has just turned around to get the skimmed milk and starts preparing your drink.

While you’re waiting for your drink, you could either chat to another person in the queue, or check your phone for any important news. So you turn around to me to strike up a conversation about the weather or the latest rumours about that M2 MacBook.

Once the barista has finished preparing your drink, they hand it over to you:

“Anything else?”

“No, thanks”

“That’ll be 2.49 then”

You pay, and the barista turns to the next person.

We’ve just experienced an asynchronous process: while the barista was busy preparing your drink, you didn’t have to stand still, holding your breath. That would’ve been pretty uncomfortable indeed. No, you were able to breathe normally, and even have a conversation with me.

Asynchronous APIs

The same applies to our apps: we don’t want the foreground process (which drives UI updates) to freeze while the app is waiting for the server to return the result of a network request. Maybe you’ve experienced this in some apps before, and lost your patience after a few seconds. When this happens, users usually either leave or kill the app. And we don’t want that to happen.

This is why many APIs that might take a bit longer are implemented asynchronously: you call them, and they immediately return control back to the caller while they start their own processing in the background. Once they finish, they will call back to the original caller. Just like in the example with the coffee bar.

Now, in a coffee bar, the barista knows who they need to get back to, as they either took your name, or they remember your face, or you’re still standing at the counter.

But how does this work in our apps? Let’s look at three different ways how you can call asynchronous APIs:

  • callbacks
  • Combine
  • async/wait

Using callbacks to call asynchronous APIs

Callbacks are probably the most commonly used way to implement asynchronous APIs, and you very likely already used them in your code. They’ve been around since the days of Objective-C (when they were called completion blocks). In Swift, we use closures to implement callbacks.

TextField("Email", text: $text)
  .onChange(of: self.text, perform: { value in 
    print(value)
  })

Swift has always supported trailing closures, making it even more elegant to use callbacks in Swift. Here’s how the above code looks like when using trailing closure syntax:

TextField("Email", text: $text)
  .onChange(of: self.text) { value in
    print(value)
  }

Notice how we were able to remove the parameter label and move the closing parenthesis right after the first parameter. This makes the code much easier to read, especially for closures that span several lines.

Internal DSLs

Being able to write code like this almost makes the function call look like it’s part of the language. The use of trailing closures is one of the key enablers for SwiftUIs DSL, and the reason why it feels so natural for writing UI code. SwiftUI is an internal DSL - it is written in the host language, making it easier to integrate in existing toolchains, and easier to learn for developers who are already used to the host language. For more details, see Martin Fowler’s article on internal DSLs.

To see this in action, let’s look at one of Firebase’s APIs. Here is a call to signIn(withEmail:password:) :

func signIn() {
  print("Executed before calling .signIn()") 2
  Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in  1
    print("Executed once .signIn() finishes, and the closure is called") 4
    if let error = error {
      print("There was an issue when trying to sign in: \(error)")
      self.errorMessage = error.localizedDescription
      return
    }
    
    guard let user = authDataResult?.user else {
      print("No user")
      self.errorMessage = "User doesn't exist."
      return
    }
    
    print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
    self.isSignedIn = true
  }
  print("Executed immediately after calling .signIn()") 3
}

Let’s discuss some of the key aspects of this code:

  1. In this code snippet, I use a trailing closure to call signIn(withEmail:password:). If you look at the method signature, you will notice there is a third parameter, completion: . Thanks to Swift’s trailing closure syntax, we can move this parameter out of the call signature and append the closure at the end of the call, outside of the parenthesis. This makes it more fluent, and pleasing to the eye.
  2. When running the code, the console output will look like this:
Executed before calling .signIn()
Executed immediately after calling .signIn()
Executed once .signIn() finishes, and the closure is called
  1. Once the print statement at the end of the function has been executed, the function will be left. If you try to access self.isSignedIn at this moment, it will still be false. Only once the user has completed the sign-in flow, and the closure has been called, the property isSignedIn will be true.

Calling asynchronous APIs using Combine

Combine is Apple’s reactive framework for handling asynchronous events. It provides a declarative API for describing how events (such as user input or network responses) should be handled.

Firebase supports Combine for some of its key APIs (such as Firebase Authentication, Cloud Firestore, Cloud Functions, and Cloud Storage).

To use Combine for Firebase, add the respective module to your target (e.g. FirebaseAuthCombine-Community) and import it.

Here’s how you can use Combine to call the signIn(withEmail:password:) method.

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

Notice how the first part of the call is virtually the same as the one we used in the previous section. This is on purpose, to make it easier to switch from one way of calling Firebase’s asynchronous APIs to another.

In the next steps, we:

  • extract the user object using Combine’s map operator (2)
  • handle errors by replacing them with a nil value (3)
  • check if the value is nil, and return true if the user object is set (4)

Finally, we assign the result (a Bool indicating whether the user has successfully signed in) to the isSignedIn property of our view model (5). As this is a publisher property, assigning a value to it will trigger SwiftUI to redraw the UI.

This code is much more compact and concise than the one we had to write when using callbacks. Instead of having to telling Swift how to process the network request and its response, Combine’s declarative programming model allows us to describe what we want to do.

Using a declarative framework for describing the data flow in your app (Combine) aligns nicely with using a declarative framework for describing the UI of your app (SwiftUI).

Calling APIs asynchronously using async/await

The final (and probably most elegant) way to call asynchronous APIs is async/await . This is a new language feature introduced in Swift 5.5 that allows us to call asynchronous code and suspend the current thread until the called code returns.

Let’s see how we can call signIn(withEmail:password:) using async/await:

@MainActor 5
func signIn() async { 2
  do {
    let authDataResult = try await 1 
      Auth.auth().signIn(withEmail: email, password: password) 3
    let user = authDataResult.user
    
    print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
    self.isSignedIn = true
  }
  catch { 4
    print("There was an issue when trying to sign in: \(error)")
    self.errorMessage = error.localizedDescription
  }
}
  1. We need to prefix the call to signIn(withEmail:password:) with await to indicate we want to call this method asynchronously. Notice that we don’t have to provide a closure - using await will suspend the thread and resume execution once the user has signed in our the process has failed for some reason. While the thread is suspended, the app’s foreground thread continues to handle events, so the app will not freeze.
  2. Using await makes our function asynchronous. To let callers know about this, we need to mark it with the async keyword. The compiler will make sure that this function is only called from another asynchronous context. More about this in a moment.
  3. Since the call to signIn(withEmail:password:) can throw an exception, we need to wrap the entire call in a do/try/catch block (3, 4).
  4. When assigning the result of the call to the isSignedIn property on our view model, we need to make sure this happens on the main thread. Instead of wrapping this assignment in a call to DispatchQueue.main.async { }, we can use the @MainActor attribute to make sure the entire function is being executed on the main thread (5).

The good news is that you can use async/await in your apps targeting iOS 13 and up - see the release notes for Xcode 13.2:

You can now use Swift Concurrency in applications that deploy to macOS Catalina 10.15, iOS 13, tvOS 13, and watchOS 6 or newer. This support includes async/await, actors, global actors, structured concurrency, and the task APIs. (70738378)

Almost all of Firebase’s asynchronous calls are ready for async/await, with a few exceptions that we’re currently fixing. Should you run into any method that you can’t seem to call using async/await, check out our issue tracker to see if we’re working on it already. If not, file an issue and we will look into it.

Closure

Making sure our apps run snappy and perform smoothly even when performing heavy duty tasks on the network is a top priority for us, no matter which platforms we’re targeting.

By providing asynchronous APIs, SDKs like Firebase and others ensure that developers can rely on a coherent and consistent programming model. This allows developers to focus on what they care about most: delivering value to their users and inspiring them with great experiences and snappy UIs.

In this post, I’ve walked you through the three most common ways you can use to access Firebase APIs asynchronously on Apple’s platforms.

As a rule of thumb, check the signature of the method you want to call. If its last parameter is a completion handler (most commonly named completion), you’re dealing with an asynchronous method that you can call with any one of the techniques describer in this article.

If you’re curious how this works in other languages, check out Doug’s article: Why are the Firebase API asynchronous?

Thanks for reading! 🔥

Newsletter
Enjoyed reading this article? Subscribe to my newsletter to receive regular updates, curated links about Swift, SwiftUI, Combine, Firebase, and - of course - some fun stuff 🎈