Swift: Working with Sign In with Apple

Rich Mucha
Nov 7 · 5 min read

Working with new Apple frameworks is becoming simpler every time they launch something new, no brainer right? Well AuthenticationServices is no exception.

What’s new?

With iOS13 came the great delights of Sign in with Apple, this is huge when building log in and account systems, even to the point of why use the old way anymore.

Sign in with Apple allows the user to create an account with either anonymous or real information; how you ask…? Well during the signup/login flow the user is presented with a modal to share information with the application:

  • First and Last name, which they can change.
  • Email, which they can use either their actual email or Apple’s new relay email. Providing the app with an email that is not their real one, but an email address that diverts to their email address until they want to turn it off.

Now I am sure you know all the above and you just want to implement, otherwise why would you be here?! So let's crack on.

Helper Implementation

Now with all new frameworks or APIs that you may use in multiple applications, you should always try and create a transferable helper class. Well it’s a personal opinion, because I don’t want to write the same code over and over again.

First lets create a new AuthenticationServicesHelper.

import AuthenticationServicesfileprivate let kAuthenticationServiceUserIdStore = "<yourKey>"@available(iOS 13.0, *)
class AuthenticationServicesHelper: NSObject {
enum State {
case active, deactive, invalid
}
// MARK: - Properties
// MARK: - Manager
private
static let manager = AuthenticationServicesHelper()
// MARK: - Helpers
}

Make sure that you mark that it is only available in iOS13 if your application supports later versions, and you import AuthenticationServices. You will also notice a kAuthenticationServiceUserIdStore this is required later in the code, you can name the key what you like.

Next in underneath the properties MARK we need a couple of parameters:

#if !os(watchOS)
private var parent: ASAuthorizationControllerPresentationContextProviding?
#endif
private var callback: ((ASAuthorizationCredential?, Error?) -> Void)?private var userId: String? {
get {
UserDefaults.standard.string(forKey: kAuthenticationServiceUserIdStore)
}
set {
UserDefaults.standard.set(newValue, forKey: kAuthenticationServiceUserIdStore)
}
}

NOTE: Forgot to say this will support watchOS so you will notice some define wrappers in places.

You will see what these parameters are for in a moment, because next we need to add some helpers:

#if os(watchOS)
static func requestAppleSignIn(completion: @escaping ((ASAuthorizationCredential?, Error?) -> Void)) {
manager.callback = completion let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
let requests = [request]
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = manager
authorizationController.performRequests()
}
#elsestatic func requestAppleSignIn(parent: ASAuthorizationControllerPresentationContextProviding, completion: @escaping ((ASAuthorizationCredential?, Error?) -> Void)) { manager.parent = parent
manager.callback = completion
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
let requests = [request] let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = manager
authorizationController.presentationContextProvider = parent
authorizationController.performRequests()
}
#endif

Both of these methods do the same thing but one is for watchOS and the other for iOS, and the difference is iOS you are required to have a UIViewController that conforms to ASAuthorizationControllerPresentationContextProviding. They create a Sign in with Apple authorisation, from presenting a modal to the user that will request the sign in details.

The completion handler, after successful validate, will return a ASAuthorizationCredential which can be cast to either:

ASPasswordCredential — The user has given an already existing user, password from their password manager. This is for older email/password based sign in’s that have been stored in a manager previously.

ASAuthorizationAppleIDCredential — The user has provided the requested credentials and it has been validate.

You will also need to added this to the bottom of the file so comform to ASAuthorizationControllerDelegate.

// MARK: - ASAuthorizationControllerDelegate@available(iOS 13.0, *)
extension AuthenticationServicesHelper: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
callback?(nil, error)
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
callback?(authorization.credential, nil)
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
userId = appleIDCredential.user
}
}
}

Ok, in both of these, the callback will be completed with the authorization.credential or an error, dependant on the results.


This is pretty much the basics and you have all the details you need to send to your backend. You might have noticed that in the didCompleteWithAuthorization delegate method the manager is holding on to the appleIDCredential.user, this is for the app to do some checks on each new session.

Once the user has logged in, it should be the apps responsibility to ensure that the userId that has been logged in is still active, because the user could have deactivated it in their iCloud settings.

We need to add a couple more helpers:

static func currentState(completion: @escaping ((State) -> Void)) {
if let id = manager.userId {
ASAuthorizationAppleIDProvider().getCredentialState(forUserID: id) { (state, error) in
switch
state {
case .authorized:
debugPrint("Sweet, nothing to do the user is authorised")
completion(.active)

default:
manager.userId = nil
completion(.invalid)
}
}
} else {
completion(.deactive)
}
}
static func deauthorize() {
manager.userId = nil
}

Upon each session of your application you should request:

AuthenticationServicesHelper.currentState { (state) in
switch
state {
case .active:
/// Great you don't need to do anything
break
default:
logout()
}
}

If the helper returns anything but .active then you need to log the user out of the account that is currently active on the device, because it means that it is not valid any longer.

You can also call the AuthenticationServicesHelper.deauthorize() if they manually logout in you application, to ensure that the userId is removed.

For the helper, that is pretty much it. So let’s take the next implementation phase:

SignIn Implementation

To allow the user to use Sign in with Apple you will need to use the provided ASAuthorizationAppleIDButton. This again is a pretty simple implementation:

Code implementation -

lazy var btnAppleSignIn: ASAuthorizationAppleIDButton = {
let btn = ASAuthorizationAppleIDButton()
btn.addTarget(self, action: #selector(appleSignIn), for: .touchUpInside)
return btn
}()
@objc
func
appleSignIn() {
AuthenticationServicesHelper.requestAppleSignIn(parent: self) { [weak self] (credential, error) in
if
let
credential = credential as? ASAuthorizationAppleIDCredential,
let authorizationCode = credential.authorizationCode,
let codeString = String(data: authorizationCode, encoding: .utf8) {
/// Send codes to the server to complete the signin/login process } else if
let
credential = credential as? ASPasswordCredential,
let username = credential.user,
let password = credential.password {

/// Send username and password to the server to complete
}}

Make sure you validate every session using the currentState method and you should be good to go.

Thanks for reading and let me know your thoughts, share your thoughts and send to friends/collegues. Also check out my other tutorials that will make your life much simpler 😁.

Happy dev’ing 😀

Rich Mucha

Written by

Founder, RichAppz Ltd

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade