Calling asynchronous Firebase APIs from Swift
Callbacks, Combine, and async/await
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.
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:
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:)
:
Let’s discuss some of the key aspects of this code:
- 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. - When running the code, the console output will look like this:
- 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 befalse
. Only once the user has completed the sign-in flow, and the closure has been called, the propertyisSignedIn
will betrue
.
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.
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’smap
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:
- We need to prefix the call to
signIn(withEmail:password:)
withawait
to indicate we want to call this method asynchronously. Notice that we don’t have to provide a closure - usingawait
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. - Using
await
makes our function asynchronous. To let callers know about this, we need to mark it with theasync
keyword. The compiler will make sure that this function is only called from another asynchronous context. More about this in a moment. - Since the call to
signIn(withEmail:password:)
can throw an exception, we need to wrap the entire call in ado/try/catch
block (3, 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 toDispatchQueue.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! 🔥
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause
SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
Styling SwiftUI Views
How does view styling work?
Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views
Asynchronous programming with SwiftUI and Combine
The Future of Combine and async/await