Integrating Sign in with Apple in UIKit and SwiftUI iOS 23.07.2021

Apple announced "Sign in with Apple" in the WWDC 2019. They offer iOS users the opportunity to create an account easy and fast without any cumbersome process steps like filling out a bunch of input fields with personal data and tapping an activation link in a received email. To use Sign in with Apple you just need an Apple-ID.

Sign In with Apple works on iOS, macOS, tvOS, and watchOS. You can also add Sign In with Apple to your website or versions of your app running on other platforms. Once a user sets up their account, they can sign in anywhere you deploy your app.

Apple added in their App Store Review Guidelines that the apps who have already 3rd party social logins (Facebook Login, Sign in with Twitter, e.t.c) to set up an account, must also add the "Sign in with Apple" method.

Developers can setup the requests so that the app can only receive the name and the email of the user and a unique opaqued user identifier.

To use Sign in with Apple as an authentication service for an app or website the user must enable the two-factor authentication for their Apple-ID.

Before getting started first of all Sign in with Apple must be enabled in your app. Go into your project and choose your project from the left side, press Signing & Capabilities, and then press + Capability to add a new capability. On the new window double click Sign in with Apple to add it. This will add an entitlement that lets your app use Sign In with Apple.

You can only test Sign in with Apple with a real device, since it needs the credentials of the currently logged in iCloud user of the device.

AuthenticationServices is the framework which needs to be used to perform anything for Sign in with Apple. Following are the usual classes with their functions of this framework:

  • ASAuthorizationAppleIDProvider. Is a mechanism to generate the requests to authenticate the user with his Apple-ID.
  • ASAuthorizationController. A controller which needs to be initialized with the requests (ASAuthorizationAppleIDRequest) and performs them. With the corresponding delegate methods you can get the credentials (ASAuthorizationAppleIDCredential) at success.
  • ASAuthorizationAppleIDCredential. The credential object contains following properties: identityToken, authorizationCode, state, user.

AuthenticationServices framework provides ASAuthorizationAppleIDButton to enables users to initiate the "Sign In with Apple" flow.

let appleButton = ASAuthorizationAppleIDButton()

Now on the press of "Sign In with Apple" button, we need to use a ASAuthorizationAppleIDProvider to create a ASAuthorizationAppleIDRequest, which we then use to initialize a ASAuthorizationController that performs the request.

@objc private func handleSignIn() {
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]

    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()
}

On success, the ASAuthorizationController delegate receives an authorization (ASAuthorization) containing a credential (ASAuthorizationAppleIDCredential) that has an opaque user identifier.

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    // handle error here
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { 
        // create an account in your system.
        let userIdentifier = appleIDCredential.user
        let userFirstName = appleIDCredential.fullName?.givenName
        let userLastName = appleIDCredential.fullName?.familyName
        let userEmail = appleIDCredential.email

        // navigate to other view controller
    } 
}

// tells the delegate from which window it should present content to the user
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
    return self.view.window!
}

Check Credential State. We can use that userIdentifier which we got from ASAuthorizationAppleIDCredential object to check the user credential state. We can get credential state for an userIdentifier by calling the getCredentialState(forUserID: completion:) method:

let appleIDProvider = ASAuthorizationAppleIDProvider()
appleIDProvider.getCredentialState(forUserID: userIdentifier) { (credentialState, error) in
    switch credentialState {
    case .authorized:
        // The Apple ID credential is valid. Show Home UI Here
        break
    case .revoked:
        // The Apple ID credential is revoked. Show SignIn UI Here.
        break
    case .notFound:
        // No credential was found. Show SignIn UI Here.
        break
    default:
        break
    }
}

Remove Existing Account from your Apple ID. The reason email and name might be nil is that user might decline to reveal these information during the Apple sign-in prompt, or the user has already signed in previously.

For testing the sign up process you can force revoke the state of the user instead of waiting. This can be done in the iOS System settings → iCloud settings → Password & Security → Apple ID logins. Your app should be listed there and then you can stop using your Apple ID for your app.

UIKit

To easy AutoLayout I'm going to use SnapKit and add all the UI elements into the view controller programmatically.

import UIKit
import SnapKit
import AuthenticationServices

struct DefaultKeys {
    static let userID = "userID"
    static let firstName = "firstName"
    static let lastName = "lastName"
    static let email = "email"
}

class ViewController: UIViewController {
    let statusLabel = UILabel()

    let signInButton: ASAuthorizationAppleIDButton = {
        //let v = ASAuthorizationAppleIDButton(type: .signIn, style: .black)
        let v = ASAuthorizationAppleIDButton()
        v.addTarget(self, action: #selector(handleSignIn), for: .touchUpInside)
        return v
    }()

    lazy var stackView: UIStackView = {
        let v = UIStackView(arrangedSubviews: [statusLabel, signInButton])
        v.axis = .vertical
        v.distribution = .fillEqually
        v.spacing = 8
        v.alignment = .center
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()

        // call the function appleIDStateRevoked if user revoke the sign in in Settings app
        NotificationCenter.default.addObserver(self, selector: #selector(appleIDStateRevoked), name: ASAuthorizationAppleIDProvider.credentialRevokedNotification, object: nil)
    }

    private func setupView() {
        view.addSubview(stackView)
        stackView.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }

        updateScreen()
    }

    private func updateScreen() {
        statusLabel.text = "Loading ..."

        if let userID = UserDefaults.standard.string(forKey: DefaultKeys.userID) {
            ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID, completion: { [weak self]
                credentialState, error in

                switch(credentialState){
                case .authorized:
                    print("user remain logged in, proceed to another view")

                    let firstName = UserDefaults.standard.string(forKey: DefaultKeys.firstName) ?? "-"
                    let lastName = UserDefaults.standard.string(forKey: DefaultKeys.lastName) ?? "-"

                    DispatchQueue.main.async {
                        self?.statusLabel.text = "Hello \(firstName) \(lastName)"
                        self?.signInButton.isHidden = true
                    }
                case .revoked:
                    print("user logged in before but revoked")
                    self?.notLoggedState()
                case .notFound:
                    print("user haven't log in before")
                    self?.notLoggedState()
                default:
                    print("unknown state")
                }
            })

        } else {
            notLoggedState()
        }
    }

    private func notLoggedState() {
        DispatchQueue.main.async {
            self.statusLabel.text = "Please sign in"
            self.signInButton.isHidden = false
        }

        UserDefaults.standard.removeObject(forKey: DefaultKeys.userID)
    }

    @objc func handleSignIn() {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]

        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }

    @objc func appleIDStateRevoked() {
        notLoggedState()
    }
}

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

extension ViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print("authorization error")
        guard let error = error as? ASAuthorizationError else {
            return
        }

        switch error.code {
        case .canceled:
            // user press "cancel" during the login prompt
            print("Canceled")
        case .unknown:
            // user didn't login their Apple ID on the device
            print("Unknown")
        case .invalidResponse:
            // invalid response received from the login
            print("Invalid Respone")
        case .notHandled:
            // authorization request not handled, maybe internet failure during login
            print("Not handled")
        case .failed:
            // authorization failed
            print("Failed")
        @unknown default:
            print("Default")
        }
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {

            let userID = appleIDCredential.user
            print("User ID: \(userID)")

            let email = appleIDCredential.email ?? ""
            print("Email: \(email)")

            let givenName = appleIDCredential.fullName?.givenName ?? ""
            print("Name: \(givenName)")

            let familyName = appleIDCredential.fullName?.familyName ?? ""
            print("Family Name: \(familyName)")

            let nickName = appleIDCredential.fullName?.nickname ?? ""
            print("Nick name: \(nickName)")

            UserDefaults.standard.set(userID, forKey: DefaultKeys.userID)
            UserDefaults.standard.set(givenName, forKey: DefaultKeys.firstName)
            UserDefaults.standard.set(familyName, forKey: DefaultKeys.lastName)
            UserDefaults.standard.set(email, forKey: DefaultKeys.email)

            updateScreen()
        }
    }
}

SwiftUI

import SwiftUI
import AuthenticationServices

struct ContentView: View {
    @AppStorage("storedName")
    private var storedName : String = "" {
        didSet {
            userName = storedName
        }
    }

    @AppStorage("storedEmail")
    private var storedEmail : String = "" {
        didSet {
            userEmail = storedEmail
        }
    }

    @AppStorage("userID")
    private var userID : String = ""

    @State
    private var userName: String = ""
    @State
    private var userEmail: String = ""

    var body: some View {
        ZStack{
            Color.white
            if userName.isEmpty {
                SignInWithAppleButton(.signIn, onRequest: onRequest, onCompletion: onCompletion)
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 200, height: 50)
            } else {
                Text("Welcome\n\(userName), \(userEmail)")
                    .foregroundColor(.black)
                    .font(.headline)
            }
        }
        .onAppear(perform: onAppear)
    }

    private func onRequest(_ request: ASAuthorizationAppleIDRequest) {
         request.requestedScopes = [.fullName, .email]
     }

     private func onCompletion(_ result: Result<ASAuthorization, Error>) {
         switch result {
         case .success (let authResults):
             guard let credential = authResults.credential
                     as? ASAuthorizationAppleIDCredential
             else { return }
             storedName = credential.fullName?.givenName ?? ""
             storedEmail = credential.email ?? ""
             userID = credential.user
         case .failure (let error):
             print("Authorization failed: " + error.localizedDescription)
         }
     }

    private func onAppear() {
        guard !userID.isEmpty else {
            userName = ""
            userEmail = ""
            return
        }

        ASAuthorizationAppleIDProvider()
            .getCredentialState(forUserID: userID) { state, _ in
                DispatchQueue.main.async {
                    if case .authorized = state {
                        userName = storedName
                        userEmail = storedEmail
                    } else {
                        userID = ""
                    }
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Register Domains and Emails for communication

The most interesting feature of Sign in with Apple is the private Apple Relay E-Mail service. Apple focusses on data privacy of the users so when the users get requested for providing their name and email they have the opportunity to hide or share their real email address with the app/website. If they choose to hide, Apple’s Relay system generates a random unique email address which routes all incoming messages and emails to the real email address of the user without the app/website knowing the real email address.

In order to contact users that use Apple's private email relay service, you need to register domains and email addresses that your organization will use for communication. To config this, open your Apple Developer Account. Now, click on More side menu on the Certificates, Identifiers & Profiles page.