iOS & Swift Tutorials

Learn iOS development in Swift. Over 2,000 high quality tutorials!

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.

4.5/5 8 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

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.

Handling Registration

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.

Handling Existing Accounts

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.

Username and Password

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.

Finish Handling Button Press

Back in ContentView.swift, you’ll need a property to store the delegate you just created. At the top of the class, add this line of code:

@State var appleSignInDelegates: SignInWithAppleDelegates! = nil

@State is how you tell SwiftUI that your struct will have mutable content which it owns and updates. All @State properties must possess an actual value, which is why the odd looking assignment to nil is present.

Now, in the same file, finish off showAppleLogin() by replacing the controller creation with this:

// 1
appleSignInDelegates = SignInWithAppleDelegates() { success in
  if success {
    // update UI 
  } else {
    // show the user an error
  }
}

// 2
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = appleSignInDelegates

// 3
controller.performRequests()

Here’s what is happening:

  1. Generate the delegate and assign it to the class’ property.
  2. Generate the ASAuthorizationController as before, but this time, tell it to use your custom delegate class.
  3. By calling performRequests(), you’re asking iOS to display the Sign In with Apple modal view.

The callback of your delegate is where you handle whatever presentation changes are necessary based on whether the end user successfully authenticated with your app.

Automate Sign In

You’ve implemented Sign In with Apple, but the user has to tap on the button explicitly. If you’ve taken them to the login page, you should see if they already configured Sign In with Apple. Back in ContentView.swift, add this line to the .onAppear block:

self.performExistingAccountSetupFlows()
Note: SwiftUI’s .onAppear { } is essentially the same thing as UIKit’s viewDidAppear(_:).

When the view appears, you want iOS to check both the Apple ID and the iCloud keychain for credentials that relate to this app. If they exist, you will automatically show the Sign In with Apple dialog, so the user doesn’t have to press the button manually. Since the button press and the automatic call with both share code, refactor your showAppleLogin method into two methods:

private func showAppleLogin() {
  let request = ASAuthorizationAppleIDProvider().createRequest()
  request.requestedScopes = [.fullName, .email]
  
  performSignIn(using: [request])
}

private func performSignIn(using requests: [ASAuthorizationRequest]) {
  appleSignInDelegates = SignInWithAppleDelegates() { success in
    if success {
      // update UI
    } else {
      // show the user an error
    }
  }

  let controller = ASAuthorizationController(authorizationRequests: requests)
  controller.delegate = appleSignInDelegates

  controller.performRequests()
}

There are no code changes other than moving the delegate creation and display into a method of its own.

Now, implement performExistingAccountSetupFlows():

private func performExistingAccountSetupFlows() {
  // 1
  #if !targetEnvironment(simulator)

  // 2
  let requests = [
    ASAuthorizationAppleIDProvider().createRequest(),
    ASAuthorizationPasswordProvider().createRequest()
  ]

  // 2
  performSignIn(using: requests)
  #endif
}

There are only a couple of steps here:

  1. If you’re using the simulator, do nothing. The simulator will print out an error if you make these calls.
  2. Ask Apple to make requests for both Apple ID and iCloud keychain checks.
  3. Call your existing setup code.

Notice how, in step 2, you didn’t specify what end-user details you wanted to retrieve. Recall earlier in the tutorial, where you learned that the details would only be provided a single time. Since this flow is used to check existing accounts, there’s no reason to specify the requestedScopes property. In fact, if you did set it here, it would simply be ignored!

Web Credentials

If you have a website dedicated to your app, you can go a little further and handle web credentials as well. If you take a peek in UserAndPassword.swift, you’ll see a call to SharedWebCredential(domain:), which currently sends an empty string to the constructor. Replace that with the domain of your website.

Now, log into your website and at the root of the site create a directory called .well-known. In there, create a new file called apple-app-site-association and paste in the following JSON:

{
    "webcredentials": {
        "apps": [ "ABCDEFGHIJ.com.raywenderlich.SignInWithApple" ]
    }
}
Note: Make sure there is no extension on the filename.

You’ll want to replace the ABCDEFGHIJ with your team’s 10-character Team ID. You can find your Team ID at https://developer.apple.com/account under the Membership tab. You’ll also need to make the bundle identifier match whatever you’re using for the app.

By taking those steps, you’ve linked Safari’s stored login details with your app’s login details. They will now be available for Sign in with Apple.

When the user manually enters a username and password the credentials will be stored so that they’re available for later use.

Runtime Checks

At any point during the lifetime of your app, the user can go into device settings and disable Sign In with Apple for your app. You’ll want to check, depending on the action to be performed, whether or not they are still signed in. Apple recommends you run this code:

let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: "currentUserIdentifier") { state, error in
  switch state {
  case .authorized:
    // Credentials are valid.
    break
  case .revoked:
    // Credential revoked, log them out
    break
  case .notFound:
    // Credentials not found, show login UI
    break
  }
}

Apple has said that the getCredentialState(forUserId:) call is extremely fast. So you should run it during app startup and any time you need to ensure the user is still authenticated. I recommend you not run at app startup unless you must. Does your app really require a logged in or registered user for everything? Don’t require them to log in until they try to perform an action that actually requires being signed in. In fact, even the Human Interface Guidelines recommend this too!

Remember that many users will uninstall a just downloaded app if the first thing they are asked to do is register.

Instead, listen to the notification that Apple provides to know when a user has logged out. Simply listen for the ASAuthorizationAppleIDProvider.credentialRevokedNotification notification and take appropriate action.

The UIWindow

At this point, you’ve fully implemented Sign In with Apple. Congratulations!

If you watched the WWDC presentation on Sign In with Apple or have read other tutorials, you might notice that there’s a piece missing here. You never implemented the ASAuthorizationControllerPresentationContextProviding delegate method to tell iOS which UIWindow to use. While technically not required if you’re using the default UIWindow, it’s good to know how to handle.

If you’re not using SwiftUI, it’s pretty simple to grab the window property from your SceneDelegate and return the value. In SwiftUI, it becomes much harder.

The Environment

A new concept with SwiftUI is the Environment. It’s an area wherein you can store data that needs to be available to many of your SwiftUI views. To some degree, you can think of it like dependency injection.

Environment Setup

Take a look at EnvironmentWindowKey.swift, and you’ll see the code necessary to store a value in the SwiftUI environment. It’s boilerplate code wherein you define the key to pass to the @Environment property wrapper and the value to be stored. Note how, since a class type is being stored, it explicitly marks the reference as weak to prevent a retain cycle.

Note: The environment code was provided by an Apple engineer in the WWDC labs

ContentView Changes

Jump back to ContentView.swift and add another property to the top of ContentView:

@Environment(\.window) var window: UIWindow?

iOS will automatically populate the window variable with the value stored in the environment.

In performSignIn(using:), modify the constructor call to pass the window property:

appleSignInDelegates = SignInWithAppleDelegates(window: window) { success in

You’ll also want to tell ASAuthorizationController to use your class for the presentationContextProvider delegate, so add this code right after assigning the controller.delegate:

controller.presentationContextProvider = appleSignInDelegates

Update the Delegate

Open SignInWithAppleDelegates.swift to handle the new property and constructor changes to the class. Replace the class definition, but not the extension with all the registration and delegate methods, with the following:

class SignInWithAppleDelegates: NSObject {
  private let signInSucceeded: (Bool) -> Void
  // 1
  private weak var window: UIWindow!

  // 2
  init(window: UIWindow?, onSignedIn: @escaping (Bool) -> Void) {
    // 3
    self.window = window
    self.signInSucceeded = onSignedIn
  }
}

Just a few changes:

  1. Store a new weak reference to the window.
  2. Add the UIWindow parameter as the first argument to the initializer.
  3. Store the passed-in value to the property.

Finally, implement the new delegate type:

extension SignInWithAppleDelegates: 
    ASAuthorizationControllerPresentationContextProviding {
  func presentationAnchor(for controller: ASAuthorizationController) 
      -> ASPresentationAnchor {
    return self.window
  }
}

The delegate just has a single method to implement that is expected to return the window, which shows the Sign In with Apple modal dialog.

Update the Scene

There’s just one piece left to getting the UIWindow into the environment. Open SceneDelegate.swift and replace this line:

window.rootViewController = UIHostingController(rootView: ContentView())

With these lines:

// 1
let rootView = ContentView().environment(\.window, window)

// 2
window.rootViewController = UIHostingController(rootView: rootView)

Two small steps do it all:

  1. You create the ContentView and append the value of the window variable.
  2. You pass that rootView variable to the UIHostingController instead of the old initializer.

The environment method returns some View which basically means it’s taking your ContentView, shoving the value you pass into that view’s environment, and then returning the ContentView back to you. Any SwiftUI View which is presented from the ContentView will now also hold that value in its environment.

If you create a new root view anywhere else, that root will not contain the environment values unless you explicitly pass them back in as well.

Logins Do not Scroll

One downside to Sign In with Apple to keep in mind is that the window that iOS displays will not scroll! For most users that won’t matter, but it’s important to note. As the owner of the site that my app uses, for example, I have numerous logins. Not only do I have the app login itself, but I’ve got a login for the SQL database, for the PHP admin site, etc.

If you’ve got too many logins, it’s possible end users won’t see what they actually need. Try to ensure that if you’re linking an app to a site that the site only has logins which will matter to the app. Don’t just bundle all your apps under a single domain.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

SignInWithAppleDelegates.swift currently returns a Boolean success, but you’ll likely want to use something more like Swift 5’s Result type so that you can return not only data from your server, but also custom error types on failure. Please see our video course, What’s New in Swift 5: Types if you’re not familiar with the Result type.

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

Average Rating

4.5/5

Add a rating for this content

8 ratings

Contributors

Comments