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.
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.