Sign in with Apple Using SwiftUI

Learn how to implement Sign in with Apple using SwiftUI, to give users more privacy and control in your iOS apps. By Scott Grosch.

4.4 (27) · 1 Review

Download materials
Save for later
Share

Sign In with Apple is a new feature in iOS 13 which allows for faster registration and authentication in your app.

While Apple repeatedly states that Sign In with Apple is straightforward to implement, there exist a few quirks to manage. In this tutorial, you’ll not only learn how to implement Sign In with Apple properly but also how to do so using SwiftUI!

You’ll need a copy of Xcode 11, a paid Apple Developer membership and an iOS 13 device to follow along with this tutorial.

Note: You’ll need an actual device running iOS 13. The simulator does not always work properly.

Getting Started

Please download the materials for this tutorial using the Download Materials button found at the top or bottom of this tutorial. Because you’ll be running on a device and dealing with entitlements, set your team identifier and update the bundle identifier appropriately, by going to the Project navigator and click the new Signing & Capabilities tab. If you build and run the app right now, you’ll see a normal looking login screen:

Note: You can ignore the two warnings Xcode displays. You’ll fix them during the tutorial.

Add Capabilities

Your provisioning profile needs to have the Sign In with Apple capability, so add it now. Click the project in the Project navigator, select the SignInWithApple target and then click the Signing & Capabilities tab. Finally, click + Capability and add the Sign In with Apple capability.

If your app has an associated website, you should also add the Associated Domains capability. This step is completely optional and not required for this tutorial, or to make Sign In with Apple function. If you do choose to use an associated domain, be sure to set the Domains value to the string webcredentials: followed by the domain name. For example, you might use webcredentials:www.mydomain.com. You’ll learn about the changes you need to make to your website later in the tutorial.

Add Sign In Button

Apple does not provide a SwiftUI View for the Sign In with Apple button, so you need to wrap one yourself. Create a new file named SignInWithApple.swift and paste this code.

import SwiftUI
import AuthenticationServices

// 1
final class SignInWithApple: UIViewRepresentable {
  // 2
  func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    // 3
    return ASAuthorizationAppleIDButton()
  }
  
  // 4
  func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
  }
}

Here’s what’s happening:

  1. You subclass UIViewRepresentable when you need to wrap a UIView.
  2. makeUIView should always return a specific type of UIView.
  3. Since you’re not performing any customization, you return the Sign In with Apple object directly.
  4. Since the view’s state never changes, leave an empty implementation.
Note: If you haven’t tried out SwiftUI yet, and want to learn more before going further, check out the tutorial SwiftUI: Getting Started.

Now that you can add the button to SwiftUI, open ContentView.swift and add it just below the UserAndPassword view:

SignInWithApple()
  .frame(width: 280, height: 60)

Apple’s style guide calls out a minimum size of 280×60, so be sure to follow that. Build and run your app, and you should see your button!

Handle Button Taps

Right now, tapping the button does nothing. Just below where you set the frame of the button, add a gesture recognizer:

.onTapGesture(perform: showAppleLogin)

And then implement showAppleLogin() after the body property:

private func showAppleLogin() {
  // 1
  let request = ASAuthorizationAppleIDProvider().createRequest()

  // 2
  request.requestedScopes = [.fullName, .email]

  // 3
  let controller = ASAuthorizationController(authorizationRequests: [request])    
}

Here’s what you’ve set up:

  1. All sign in requests need an ASAuthorizationAppleIDRequest.
  2. Specify the type of end user data you need to know.
  3. Generate the controller which will display the sign in dialog.

You should request only user data which you need. Apple generates a user ID for you. So, if your only purpose in grabbing an email is to have a unique identifier, you don’t truly need it — so don’t ask for it. ;]

ASAuthorizationControllerDelegate

When the user attempts to authenticate, Apple will call one of two delegate methods, so implement those now. Open SignInWithAppleDelegates.swift. You’ll implement the code here which runs after the user taps the button. While you could implement the code right in your view, it’s cleaner to place it elsewhere for reusability.

You’ll just leave the authorizationController(controller:didCompleteWithError:) empty for now, but in a production app, you should handle these errors.

When authorization is successful, authorizationController(controller:didCompleteWithAuthorization:) will be called. You can see in the downloaded sample code there are two cases you’ll want to handle. By examining the credential property, you determine whether the user authenticated via Apple ID or a stored iCloud password.

The ASAuthorization object passed to the delegate method includes any properties you asked for, such as the email or name. The existence of the value is how you can determine whether this is a new registration or an existing login.

Note: Apple will only provide you the requested details on the first authentication.

The preceding note is critical to remember! Apple assumes that you’ll store the details you asked for and thus not require them again. This is one of the quirks of Sign In with Apple that you need to handle.

Consider the case where a user is signing in for the first time, so you need to perform registration. Apple hands you the user’s email and full name. Then, you attempt to call your server registration code. Except, your server isn’t online or the device’s network connection drops, etc.

The next time the user signs in, Apple won’t provide the details because it expects you already possess them. This causes your “existing user” flow to run, resulting in failure.

In authorizationController(controller:didCompleteWithAuthorization:), just inside the first case statement, add the following:

// 1
if let _ = appleIdCredential.email, let _ = appleIdCredential.fullName {
  // 2
  registerNewAccount(credential: appleIdCredential)
} else {
  // 3
  signInWithExistingAccount(credential: appleIdCredential)
}

In this code:

  1. If you receive details, you know it’s a new registration.
  2. Call your registration method once you receive details.
  3. Call your existing account method if you don’t receive details.

Paste the following registration method at the top of the extension:

private func registerNewAccount(credential: ASAuthorizationAppleIDCredential) {
  // 1
  let userData = UserData(email: credential.email!,
                          name: credential.fullName!,
                          identifier: credential.user)

  // 2
  let keychain = UserDataKeychain()
  do {
    try keychain.store(userData)
  } catch {
    self.signInSucceeded(false)
  }

  // 3
  do {
    let success = try WebApi.Register(
      user: userData,
      identityToken: credential.identityToken,
      authorizationCode: credential.authorizationCode
    )
    self.signInSucceeded(success)
  } catch {
    self.signInSucceeded(false)
  }
}

There are a few things occurring here:

  1. Save the desired details and the Apple-provided user in a struct.
  2. Store the details into the iCloud keychain for later use.
  3. Make a call to your service and signify to the caller whether registration succeeded or not.

Notice the usage of credential.user. This property contains the unique identifier that Apple assigned to the end-user. Utilize this value — not an email or login name — when you store this user on your server. The provided value will exactly match across all devices that the user owns. In addition, Apple will provide the user with the same value for all of the apps associated with your Team ID. Any app a user runs receives the same ID, meaning you already possess all their information on your server and don’t need to ask the user to provide it!

Your server’s database likely already stores some other identifier for the user. Simply add a new column to your user type table which holds the Apple-provided identifier. Your server-side code will then check that column first for a match. If not found, revert to your existing login or registration flows, such as using an email address or login name.

Depending on how your server handles security, you may or may not need to send the credential.identityToken and credential.authorizationCode. OAuth flows use those two pieces of data. OAuth setup is outside the scope of this tutorial.

Note: Apple is providing the data needed to generate their public key with the OAuth details. Essentially, they’re giving you a JSON Web Key (JWK).

To ensure proper storage in the keychain, edit UserDataKeychain.swift in CredentialStorage and update account to have the bundle identifier for your app and then append any other text value. I like to append .Details to the bundle identifier. What matters is that the account property and bundle identifier do not exactly match, so the stored value is only used for the purpose for which you intend it.

As previously explained, when an existing user logs into your app, Apple doesn’t provide the email and full name. Add this method right below the registration method in SignInWithAppleDelegates.swiftto handle this case:

private func signInWithExistingAccount(credential: ASAuthorizationAppleIDCredential) {
  // You *should* have a fully registered account here.  If you get back an error
  // from your server that the account doesn't exist, you can look in the keychain 
  // for the credentials and rerun setup

  // if (WebAPI.login(credential.user, 
  //                  credential.identityToken,
  //                  credential.authorizationCode)) {
  //   ...
  // }

  self.signInSucceeded(true)
}

The code you place in this method will be very app-specific. If you receive a failure from your server telling you the user is not registered, you should query your keychain, using retrieve(). With the details from the returned UserData struct, you then re-attempt registration for the end user.

The other possibility, when using Sign In with Apple, is the end user will select credentials which are already stored in the iCloud keychain for the site. In the second case statement for authorizationController(controller:didCompleteWithAuthorization:) add the following line:

signInWithUserAndPassword(credential: passwordCredential)

And then just below signInWithExistingAccount(credential:) implement the appropriate method:

private func signInWithUserAndPassword(credential: ASPasswordCredential) {
  // if (WebAPI.login(credential.user, credential.password)) {
  //   ...
  // }
  self.signInSucceeded(true)
}

Again, your implementation will be app-specific. But, you’ll want to call your server login and pass along the username and password. If the server fails to know about the user, you’ll need to run a full registration flow as you don’t possess any details available from the keychain for their email and name.