Sign in with Apple using SwiftUI and Firebase

Replicating the iOS Reminders App, Part 3

Mar 24, 2020 • 23 min read

This article is part of a series of articles that explores building a real-world application using SwiftUI, Firebase, and a couple of other technologies.

Here is an overview of the series and what we’re going to build:

  • In part 1 of the series, we focussed on building the UI with SwiftUI, using a simple data model.
  • In part 2, we connected the application to Firebase, and synchronized the user’s tasks with Cloud Firestore
  • In part 3 (which you are reading right now), we’re going to implement Sign in with Apple, allowing users to sign in from multiple devices to access their data, and laying the groundwork for advanced features such as sharing tasks with your family and co-workers

So, let’s get started!

Why are we doing this?

Before we dive into implementing Sign in with Apple, let’s take a step back and look at what we did in the past episode. So far, users of our application can enter new tasks, mark them as done, or modify them. All tasks the user enters are synchronised with Cloud Firestore.

We’re using Firebase Anonymous Authentication to get a unique ID for each user to make sure we can store data per user, and don’t accidentally mix up the tasks of person A and B (I’m pretty sure my kids would love to see their chores end up on my task list, but generally, this is not how task lists work - sorry to break the bad news to you…).

Now you might be wondering how Anonymous Auth determines this unique user ID, and some of you might suspect Firebase is using the device ID or some other user- or device-specific feature to compute the user ID. For a number of reasons, this is not what Firebase does. Instead, when you ask Firebase Auth to sign in a user anonymously, the SDK calls an API endpoint that returns a globally unique ID. This ID will then be stored in the local keychain, to make it easier to transparently sign in the user when they return to the app.

This works great for users who only use one device, but it has one significant drawback: you will be assigned a new user ID every time you sign in to the app on a new device. Why is that?

Essentially, what happens is this: when calling Auth.auth().signInAnonymously(), the Firebase SDK will check the iOS keychain to see if the user has already signed in. If so, it will use this ID to sign in the user transparently. If not, it will call the API endpoint to generate a new user ID.

This means: you will appear to be a new user whenever you launch the app on a different device. That’s bad news for all of us who would like to use multiple devices to manage their tasks, e.g. their iPhone when they’re on the go, or their iPad when chilling out at home (I hear WFH seems to be rather popular these days…).

Thankfully, with Firebase it is easy to upgrade an anonymous user to a permanent user, making it possible to support use cases like:

  • accessing the same data from different devices (and, of course, operating systems)
  • sharing tasks with other users (e.g. to set up a shared shopping list)
  • sending users an email every morning with an overview of their due tasks

Firebase Authentication supports a wide range of sign-in providers, such as Email/Password, Phone Number, Google Sign-in, Facebook Login, Twitter Login, or Sign in with Apple. If none of the default providers match your needs, you can even implement your own provider and plug it into Firebase Auth using Custom Auth.

What we’re going to cover

In this article, we’re going to specifically look into how to implement Sign in with Apple and how to connect it to Firebase Authentication. I will show how to integrate with other Sign-in providers in subsequent articles. A lot has been written about Sign in with Apple already, so we will gloss over some of the basic parts, but I will link to other articles in the resources section, so if you would like to read up on the basics, I’d encourage you to do so.

Integrating Sign in with Apple with SwiftUI (which is the UI technology I decided to use for our demo app) has a number of challenges which we will cover in this article.

Specifically, we’ll look at the following:

  • How to implement the Sign in with Apple button in SwiftUI
  • How to handle the authorization flow and how to implement ASAuthorizationControllerDelegate in a SwiftUI application
  • How to upgrade an anonymous user to a permanent user
  • What happens with the user’s data when upgrading an anonymous user
  • What happens if you’re trying to link to an account that has already been upgraded

Basic setup

If you’re following along, check out the tag stage_4/sign_in_with_apple/start and open MakeItSo.xcworkspace in the final folder.

To use Sign in with Apple with Firebase Auth, there are a few preliminary steps you need to take:

  • First of all, add the Sign in with Apple capability to your Xcode project Adding the Sign in with Apple capability

  • In the Firebase Console, enable Sign in with Apple in the Authentication section Enabling Sign in with Apple in the Firebase console

  • (In case you haven’t already done this) make sure to add your iOS app to your Firebase project. We already did this in part 2 of the series, which also contains a link to a video I made about this. Check it out here.

With that out of the way, let’s write some code!

Implementing the Sign in with Apple button

Apple is very specific about how the Sign in with Apple button should look like (see the HIG). To make things easier, they’ve provided ASAuthorizationAppleIDButton, which goes a long way: for example, the button uses Apple-approved fonts and icons, maintains ideal content dimensions, and (maybe most importantly) supports internationalisation right out of the box. So instead of creating a custom button (which is explained in the same HIG document a bit further down), let’s use the Apple-provided class.

However, there is no SwiftUI equivalent of ASAuthorizationAppleIDButton, so we’ll have to use UIViewRepresentable to wrap it. We’ll also want to make sure that the button automatically adjusts to light/dark mode.

Create a new file SignInWithAppleButton.swift and paste the following code:

import SwiftUI
import AuthenticationServices
// Implementation courtesy of
struct SignInWithAppleButton: View {
  @Environment(\.colorScheme) var colorScheme: ColorScheme // (1)
  var body: some View {
    Group {
      if colorScheme == .light { // (2)
        SignInWithAppleButtonInternal(colorScheme: .light)
      else {
        SignInWithAppleButtonInternal(colorScheme: .dark)
fileprivate struct SignInWithAppleButtonInternal: UIViewRepresentable { // (3)
  var colorScheme: ColorScheme
  func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    switch colorScheme {
    case .light:
      return ASAuthorizationAppleIDButton(type: .signIn, style: .black) // (4)
    case .dark:
      return ASAuthorizationAppleIDButton(type: .signIn, style: .white) // (5)
    @unknown default:
      return ASAuthorizationAppleIDButton(type: .signIn, style: .black) // (6)
  func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
struct SignInWithAppleButton_Previews: PreviewProvider {
  static var previews: some View {

A few quick remarks about this specific implementation:

  • We subscribe to the current colorScheme using an Environment object (1)
  • This allows us to force SwiftUI to re-create the button whenever the color scheme changes - see (2)
  • SignInWithAppleButtonInternal is the class that actually implements UIViewRepresentable (3)
  • In makeUIView, we create an instance of ASAuthorizationAppleIDButton according to the current colorScheme (4, 5, 6)
  • As we don’t expose any properties that would require us to update the button whenever they change, there is no need to implement updateUIView.

We can now use the Sign in with Apple button like any other SwiftUI view. Let’s create a simple sign-up screen that explains the benefits of signing up to the user, and add the Sign in with Apple button to the bottom of the screen. In a later episode in this series, we’ll add more buttons to allow the user to sign in with other authentication providers.

import SwiftUI
import Firebase
import AuthenticationServices
import CryptoKit
struct SignInView: View {
  @Environment(\.window) var window: UIWindow?
  @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
  @State var signInHandler: SignInWithAppleCoordinator?
  var body: some View {
    VStack {
      Text("Create an account to save your tasks and access them anywhere.")
        .padding(.top, 20)
        .frame(width: 280, height: 45)
        .onTapGesture { // (1)
          self.signInWithAppleButtonTapped() // (2)
      // other buttons will go here
  func signInWithAppleButtonTapped() {
    signInHandler = SignInWithAppleCoordinator(window: self.window)
    signInHandler?.link { (user) in
      print("User signed in \(user.uid)")
      self.presentationMode.wrappedValue.dismiss() // (3)
struct SignInView_Previews: PreviewProvider {
  static var previews: some View {

When the user taps (1) on the Sign in with Apple button, we will kick off (2) the authentication flow. Once the flow finishes, we will dismiss the sign-in view and return to the parent view (3).

All this isn’t terribly exciting yet, so let’s take a closer look at the sign-in flow itself.

Handling the Sign in with Apple flow

Handling the Sign in with Apple flow is a three-step process:

  • First, we need to create an ASAuthorizationAppleIDRequest that contains the scopes we’re interested in as well as some security-specific fields

  • Next, we need to create an ASAuthorizationController, pass in the request, and call performRequests()

  • ASAuthorizationController will do all the heavy lifting of interacting with the user. Once it has navigated the user through the process of signing in, it will invoke one of the callbacks:

    1. didCompleteWithAuthorization: if things went well
    2. didCompleteWithError: if there was an issue

Let’s dive in a little deeper and see what’s going on in each of these steps!

Coordinating the Sign-in Flow

In a UIKit application, you might put most of the code for handling Sign in with Apple in a view controller. With SwiftUI, this isn’t possible, as we do not have a view controller. This actually is a good thing (tm), as it will prevent us from ending up with a Massive View Controller (MVC). Instead, let’s create a class SignInWithAppleCoordinator and a couple of extensions to organize the code in a more maintainable way.

enum SignInState: String {
  case signIn
  case link
  case reauth
class SignInWithAppleCoordinator: NSObject {
  @LazyInjected private var taskRepository: TaskRepository
  @LazyInjected private var authenticationService: AuthenticationService // (1)
  private weak var window: UIWindow!
  private var onSignedIn: ((User) -> Void)? // (2)
  private var currentNonce: String? // (3)
  init(window: UIWindow?) {
    self.window = window
  // more code to follow

Let me explain the code:

  • AuthenticationService (1) is a class we implemented in part 2 of this series - it manages the sign-in flow and keeps track of the currently signed in user.
  • We define a callback (2) that we store in onSignedIn so that we can inform the caller when the user has signed in.
  • currentNonce (3) holds a cryptographic nonce - a value that can be used exactly once to prove that the request was actually sent by the current client. More on this in a minute.
  • We hold on to the window that owns the current view - we will need to return this in our implementation of ASAuthorizationControllerPresentationContextProviding.

Creating the request

ASAuthorizationController is the core class handling the authorization flow for Sign in with Apple. Since it is capable of handling different types of authorization flows (password, single sign-on, and signing in with an Apple ID), it needs to be configured using an ASAuthorizationRequest.

We do this in a helper method, appleIDRequest, by creating ASAuthorizationAppleIDRequest (which is a subclass of ASAuthorizationRequest):

class SignInWithAppleCoordinator: NSObject {
  // ...
  private func appleIDRequest(withState: SignInState) -> ASAuthorizationAppleIDRequest {
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest() // (1)
    request.requestedScopes = [.fullName, .email] // (2)
    request.state = withState.rawValue3
    let nonce = randomNonceString() // (4)
    currentNonce = nonce
    request.nonce = sha256(nonce)
    return request
  // ...

After creating the request using ASAuthorizationAppleIDProvider.createRequest() (1), we configure a couple of things:

  • First, we indicate that we’re interested in the user’s full name and their email address by providing the .fullName and .email scopes (2).
  • We also pass in a SignInState (3) - this is an enum that indicates which type of authorization flow we’re using (signing up, logging in, re-authenticating).
  • As briefly mentioned above, the nonce (4) is a one-time key: we will compare the nonce in the private field currentNonce to the nonce we receive in didCompleteWithAuthorization to verify that it was actually us who sent the request.

Here is the code to generate the nonce and compute it’s secure hash code (using the SHA-256 algorithm):

// Adapted from
private func randomNonceString(length: Int = 32) -> String {
  precondition(length > 0)
  let charset: Array<Character> =
  var result = ""
  var remainingLength = length
  while remainingLength > 0 {
    let randoms: [UInt8] = (0 ..< 16).map { _ in
      var random: UInt8 = 0
      let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
      if errorCode != errSecSuccess {
        fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
      return random
    randoms.forEach { random in
      if remainingLength == 0 {
      if random < charset.count {
        remainingLength -= 1
  return result
@available(iOS 13, *)
private func sha256(_ input: String) -> String {
  let inputData = Data(input.utf8)
  let hashedData = SHA256.hash(data: inputData)
  let hashString = hashedData.compactMap {
    return String(format: "%02x", $0)
  return hashString

Usually, when you authenticate a user, you will use Auth.auth().signIn() to sign them in to Firebase.

In our case, however, we’re in a different situation: the user already signed in - using Firebase Anonymous Authentication! This means we already have a User instance, and the user potentially already has created a number of to-do items that are associated with the anonymous user’s userID in our Cloud Firestore instance. If we were to use the signIn() method to sign in to another account, we’d end up with orphaned data, i.e. all those documents would still be stored in Firestore, but they’d be assigned to an anonymous account that the user no longer can sign in to. Thus, the user would lose access to them.

Instead, we will use a Firebase Authentication feature called Account Linking to upgrade the user’s anonymous account into a permanent account based on their Apple ID.

Now I should give you a word of warning: Apple requires that you get the user’s consent when you’re linking their Apple ID with any directly identifiable personal information such as their email address or phone number. This applies when you’re linking to an authentication provider such as Facebook Login, Google Sign-in, or Email/Password Auth. However, as an anonymous user is anonymous (duh) and doesn’t contain any personal identifiable information, there’s no need for us to get the user’s consent.

To kick off the linking flow, we call the link() method on our SignInWithAppleCoordinator class:

class SignInWithAppleCoordinator: NSObject {
  // ...
  func link(onSignedIn: @escaping (User) -> Void) {
    self.onSignedIn = onSignedIn // (1)
    let request = appleIDRequest(withState: .link)
    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self // (2)
    authorizationController.presentationContextProvider = self // (3)
  // ...

We’re storing the callback (which is implemented as a trailing closure) in the onSignedIn property (1) so we can inform the caller about the result of the authentication flow. Also note that both the delegate (2) and the presentationContextProvider (3) are set to self, so we can handle all of ASAuthorizationController’s callbacks in one spot.

Handling the callbacks

Depending on the events that happen while the user interacts with the ASAuthorizationController, we will receive some callbacks. Let’s take a look at what might happen.

extension SignInWithAppleCoordinator: ASAuthorizationControllerDelegate {
  func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
      guard let nonce = currentNonce else { // (1)
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      guard let appleIDToken = appleIDCredential.identityToken else { // (2)
        print("Unable to fetch identity token")
      guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else { // (3)
        print("Unable to serialise token string from data: \(appleIDToken.debugDescription)")
      guard let stateRaw = appleIDCredential.state, let state = SignInState(rawValue: stateRaw) else {
        print("Invalid state: request must be started with one of the SignInStates")
      // ...

First of all, we’ll need to perform some sanity and security checks:

  • Does the credential contain a nonce (1)?
  • Does the authorization response contain an ID token (2)?
  • Does the authorization response contain the state we sent (3)?

If all of the above holds true, we can continue by requesting Firebase’s OAuthProvider to mint a credential based on the authentication provider (, the ID token and the nonce:

      // ...
      let credential = OAuthProvider.credential(withProviderID: "",
                                                idToken: idTokenString,
                                                rawNonce: nonce)
      // ...

Depending on the type of authorization flow we’re interested in, we will then either perform a regular sign-in by calling Auth.auth().signIn(), or link the anonymous user account with the freshly minted credential by calling Auth.auth()

Let’s look at the code for the linking flow:

      // ... credential) { (result, error) in // (1)
        if let error = error, (error as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue { // (2)
          print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(currentUser.uid)] tasks.")
          if let updatedCredential = (error as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
            print("Signing in using the updated credentials")
            Auth.auth().signIn(with: updatedCredential) { (result, error) in
              if let user = result?.user {
                // TODO: handle data migration
                self.doSignIn(appleIDCredential: appleIDCredential, user: user) // (3)
        else if let error = error {
          print("Error trying to link user: \(error.localizedDescription)")
        else {
          if let user = result?.user {
            self.doSignIn(appleIDCredential: appleIDCredential, user: user) // (3)
    // ...

That’s quite a bit, so let’s take a closer look at what’s happening.

  • As we decided to upgrade the existing anonymous user to a permanent user (based on their Apple ID credentials), we’re calling credential) (1).
  • The result of that call either is an upgraded user or an error (2).
  • The most common cause for an error is if the user is trying to link with an Apple ID that they’ve already linked to. As developers, we will run into this situation all of the time, as we’re obviously linking to the Apple ID of our own account or a test account (should you have one). I’ll show you how to unlink your Apple ID from your application below to help you test all scenarios, but we need to gracefully deal with this situation. We’ve got a couple of options, such as telling the user they cannot do this (bad), or signing in to the already linked account (better), or signing in to the already linked account and migrating any data the user might have in the anonymous user they’re currently using (best). Because this is a rather involved topic, we’re going to cover this is a separate post.
  • If all goes well (i.e. the user hadn’t linked their Apple ID before), the result of credentials) is a signed-in User instance, representing the user’s Apple ID. In this case, we can go ahead and call the callback provided when the caller kicked off the sign-in process (3).

Unlinking your Apple ID from an app that uses Sign in with Apple

Once you’ve signed in to your app, you cannot repeat the sign-in flow, which makes testing a bit of a challenge. To get back to the initial state, you need to disconnect your Apple ID from your app. Here is how:

  • Go to and sign in using your Apple ID
  • In the Security section, find Apps & Websites using Apple ID and click on Manage…
  • You will see a pop-up dialog that shows you all apps that are connected to your Apple ID Managing apps using Apple ID
  • Click on the name of the app you want to disconnect
  • The following dialog will tell you when you first started using your Apple ID with this application Disconnecting your app
  • Click on Stop using Apple ID to disconnect this app from your Apple ID

The next time you open that app, you will need to sign in again.

Retrieving the user’s display name

Many apps provide a more personalised experience for their signed in users, for example by displaying their name on the profile screen.

Sign in with Apple will provide the user details you requested using the scopes only the first time a user signs in to your app with their Apple ID. This is a design decision Apple made to ensure developers take privacy concerns seriously.

Firebase automatically stores the opaque user identifier Apple provides in the Firebase user that is created when you invoke or Auth.auth().signIn(). The same applies to the user’s email address. Keep in mind that if the user decides to use Apple’s anonymous email relay, the user’s email address will look like this: <random-identifier>

The user’s full name is not automatically stored in the user object that Firebase creates for a linked user. This is a design decision that the Firebase team made to prevent developers from accidentally violating Apple’s anonymised data requirements.

As our to-do-list app does not support linking with any other identity provider other than Sign in with Apple, we won’t be running into this situation. So let’s store the user’s full name into the Firebase user object ourselves:

extension SignInWithAppleCoordinator: ASAuthorizationControllerDelegate {
  // ...
  private func doSignIn(appleIDCredential: ASAuthorizationAppleIDCredential, user: User) {
    if let fullName = appleIDCredential.fullName {
      if let givenName = fullName.givenName, let familyName = fullName.familyName {
        let displayName = "\(givenName) \(familyName)"
        self.authenticationService.updateDisplayName(displayName: displayName) { result in // (1)
          switch result {
          case .success(let user):
            print("Succcessfully update the user's display name: \(String(describing: user.displayName))")
          case .failure(let error):
            print("Error when trying to update the display name: \(error.localizedDescription)")
          self.callSignInHandler(user: user)
      else {
        self.callSignInHandler(user: user)
  private func callSignInHandler(user: User) {
    if let onSignedInHandler = self.onSignedInHandler {
class AuthenticationService: ObservableObject {
  // ...
  func updateDisplayName(displayName: String) { // (2)
    if let user = Auth.auth().currentUser {
      let changeRequest = user.createProfileChangeRequest() // (3)
      changeRequest.displayName = displayName
      changeRequest.commitChanges { error in // (4)
        if error != nil {
          print("Successfully updated display name for user [\(user.uid)] to [\(displayName)]")

We first retrieve the user’s full name from the ASAuthorizationAppleIDCredential we received when the Sign in with Apple flow completed successfully (1). Then, in AuthenticationService.updateDisplayName (2), we create a UserProfileChangeRequest (3), set the user name, and commit the changes to Firebase Authentication (4).

Putting it all together

At this stage, we’ve got an authentication solution for our app that will allow users to start using the app without having to sign in. Once they want to use their to-do list on a second device, they can sign in to the app on the first device using Sign in with Apple. At this point, their anonymous user will be converted into a permanent account that is connected to the user’s Apple ID via Sign in with Apple.

As their Firebase user ID hasn’t changed by this upgrade, all data they have previously entered using their anonymous account is still stored in Cloud Firestore, but is now accessible to their new, permanent account.

This means they can now sign in to their account on a different device and access the same data. Thanks to Cloud Firestore’s realtime capabilities, any change the users makes on one device will be synchronised to any other device they are signed in to and will be visible almost instantaneously.

Here is a screencast showing two simulators side by side showing how this looks like:

Get the completed source code for the app by checking out the tag stage_4/sign_in_with_apple/end and opening MakeItSo.xcworkspace in the final folder.

Recommendation for implementing authentication for your app

User on-boarding is a critical moment in an app’s lifecycle, and it’s easy to screw up and lose users instead of retaining them. One of the biggest mistakes is to ask users to register for using your app before you’ve given them the opportunity to experience the app and properly understand its value proposition. Asking users to sign in before they can start using your app puts up a pretty serious speed bump, and your potential users will ask themselves if they want to make a commitment to your app. If you haven’t given them an incentive, they are very likely going to uninstall your app.

Implementing a local data storage solution that allows users to work locally first and only later synchronise their data with your backend (which requires asking them to sign in) is a serious piece of work, and it’s easy to see why a lot of developers would rather try to avoid having to implement this.

By using Firebase Anonymous Authentication to transparently create a Firebase user account, and storing the users’ data in Firebase Realtime Database or Cloud Firestore, it becomes a lot easier to implement such a solution:

  • Use Firebase Anonymous Auth to sign in the user transparently
  • Use their anonymous user ID to store data in Firebase RTDB or Cloud Firestore
  • Once the user has understood the value proposition of your application, suggest creating an account
  • Use one of the identity providers that Firebase supports (such as Sign in with Apple, Google Sign-in, or Facebook Login), or Email/Password authentication to let the user sign in
  • The anonymous user will be upgraded to a permanent user with the same user ID. This means all existing data is safe and can now be used by the user on any of the devices they sign in to using their preferred authentication mechanism.

Firebase supports a number of authentication mechanisms, and you can mix and match which one you’d like to support in your app. Please keep in mind though, that you absolutely must support the same set of authentication mechanisms on all platforms your app is available on.

Otherwise, users might sign in using Apple on iOS, and later be stuck when they try to sign in to your application on Android, but you only support Google Sign-in on Android. Or worse - they signed in using an identity provider on one platform, but you only support Email/Password sign-in on your other platforms. How should they sign in to their (existing) account using a password if they didn’t even choose a password in the first place?


In this article, you saw how to

  • Implement Sign in with Apple
  • Upgrade anonymous users to permanent users by using a mechanism called account linking
  • Learned about some guiding principles to ensure a great UX for your on-boarding and authorisation flow

Implementing a solid authentication solution can be a challenging task, but I hope you have seen how Firebase can make this a lot easier for you by providing a solid framework for implementing authorisation solutions.

Over the course of the past three articles, we’ve managed to implement a fully functional to-do list application, but there is still a lot to do. Here are a couple of ideas for things that we will look into in the next episodes:

  • Migrating an anonymous user’s data in case they already signed in with Apple on another device
  • Showing the user’s due tasks in a Today Extension
  • Allowing users to add new tasks via a Siri Extension
  • Sharing lists with other users
  • Uploading attachments to tasks
  • Properly implementing due dates (and sending users a notification for any tasks due today)

If you’ve got any features you’d like to see me implement, please let me know by sending me a tweet or filing an issue on the project’s repository on GitHub.

Thanks for reading!


Source code
You can find the source code for this post in this GitHub repo.
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 🎈