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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.