Categories
iOS SwiftUI

SwiftUI Sign In With Apple

During WWDC 2019, Apple introduced Sign in with Apple to allow developers to set up a fast and secure sign-in in their applications. It makes sense in streamlining the sign-in experience as applications downloaded from the App Store require Apple ID authentication as well.

SwiftUI has rocked the iOS development community since it got its first glimpse. It’s here to stay, thanks to its state-driven framework which lets us write “What to do” instead of “How to do”.

Sign in with Apple not only ensures privacy by not tracking a user’s sensitive information but also provides uniformity to the user by syncing seamlessly across all your devices through iCloud keychain.

Yes, it offers browser support on Android and Windows devices as well through Sign in with Apple JS. Moreover, it bumps up the security by incorporating on-device machine learning for fraud detection (knowing if the user is real).

Hide My Email Address

While signing in with apple, you can choose to hide your email from the application.

Apple then creates a random but real and verified email address that routes to your real one. Although you can choose to stop receiving emails from the relayed address. This verified email address feature is a win-win for users as well as developers.

Our Goal

  • Integrating Sign in with Apple in a SwiftUI based application
  • Setting up the authorization flow and storing user credentials
  • Handling authorization changes

Pre-requisites: An Apple developer account is required for enabling Sign in with Apple capabilities.

Enabling Sign In With Apple Capability

To start off, you need to enable the Sign in with Apple capability from the Target | Capabilities panel as shown below:

swiftui-sign-in-with-apple-app-capability

This creates an entitlements file containing the Sign in with Apple, access level set to default. Now we’re ready to add the button in our SwiftUI application.

Integrating Sign In With Apple Button

To add a Sign in with Apple button, you need to import AuthenticationServices. The following code adds a ASAuthorizationAppleIDButton to the SwiftUI view:

struct ContentView: View {
var body: some View {
SignInWithAppleView()
            .frame(width: 200, height: 50)
    }
}
struct SignInWithAppleView: UIViewRepresentable {
func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        ASAuthorizationAppleIDButton()
    }
    
    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
    }
}

SwiftUI doesn’t provide a Sign in with Apple View currently, so we’ve used a UIViewRepresentable subclass to wrap our button in a UIView.

Apple allows us to customize the button color, corner radius, and text to suit our app designs in the following ways:

swiftui-sign-in-with-apple-button-custom
let button = ASAuthorizationAppleIDButton(authorizationButtonType: .signUp, authorizationButtonStyle: .whiteOutline)
button.cornerRadius = 10

We can set the button type to be continue, signUp, and signIn and the style as white, black, or whiteOutline.

Now that our button is set, it’s time to configure it.

Handling Button Selectors

We can set the tap functionality on the button either in the SwiftUI view or the UIKit view. We’re going with the latter as shown in the code snippet below:

struct SignInWithAppleView: UIViewRepresentable {

@Binding var name : String

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        let button = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: .black)

        button.addTarget(context.coordinator, action:  #selector(Coordinator.didTapButton), for: .touchUpInside)
        return button
    }

    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {

    }
}

We’ve set a binding property wrapper name which will play a key role in updating SwiftUI after the authorization is completed. For now, just keep a note of this.

Next, we’ll set up the authorization flow on the button tap in our Coordinator class!

Displaying Authorization Dialog

To display the authorization dialog, we need to create a request using ASAuthorizationAppleIDProvider and feed it into ASAuthorizationController as shown below:

@objc func didTapButton() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.presentationContextProvider = self
authorizationController.delegate = self
authorizationController.performRequests()
}

We need to implement the following two protocols in our Coordinator class:

  • ASAuthorizationControllerPresentationContextProviding
  • ASAuthorizationControllerDelegate

The former one is used for displaying the authorization dialog on the screen and the latter handles the authorization result based on the user’s action.

swiftui-sign-in-with-apple-authentication

Handling the User’s Credentials After Authorization

class Coordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    let parent: SignInWithAppleView?
    
    init(_ parent: SignInWithAppleView) {
        self.parent = parent
        super.init()

    }
    
    @objc func didTapButton() {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.presentationContextProvider = self
        authorizationController.delegate = self
        authorizationController.performRequests()
    }
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        let vc = UIApplication.shared.windows.last?.rootViewController
        return (vc?.view.window!)!
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else {
            print("credentials not found....")
            return
        }
        
        let defaults = UserDefaults.standard
        defaults.set(credentials.user, forKey: "userId")
        parent?.name = "\(credentials.fullName?.givenName ?? "")"
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error){
    }
}

In the above code, the didCompleteWithAuthorization is triggered once the user’s authorization is completed. It returns the username, an identifier, and the user’s email address in the credential argument.

Typically, Keychain is used for saving the credentials but for the sake of simplicity of this article, we’ll use the UserDefaults.

To update the SwiftUI ContentView, we’ll set the Binding instance parent.name with the user name.

struct ContentView: View {
    
    @State var name : String = ""
    @EnvironmentObject var authorizationStatus: UserSettings
    
    var body: some View {
         VStack{
            if self.name.isEmpty{
                SignInWithAppleView(name: $name)
                .frame(width: 200, height: 50)
            }
            else{
                Text("Welcome\n\(self.name)")
                    .font(.headline)
            }
        }
    }
}

In the above code, based on the state name, we update the text and remove the Sign in with Apple button now that our authorization is completed. I think I know what you’re thinking now…Why is there an environment object declared?

EnvironmentObject are dynamic view properties. Similar to ObjectBinding (external property) and States(internal property), except that environment objects allow passing bindable objects to views without explicitly passing the complete object.

Handling Authorization Changes

In our case, we need EnvironmentObject for handling authorization changes and checking for already-authenticated users.

For this, add a UserSetting class in your ContentView.swift file:

class UserSettings: ObservableObject {

// 1 = Authorized, -1 = Revoked 
@Published var authorization: Int = 0

}

Next, we’ll check for any pre-existing users or authorization changes (users can revoke Sign in with Apple from Settings | Apple ID | Password And Security) in the SceneDelegate class and update the environment object accordingly.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    
        if let userID = UserDefaults.standard.object(forKey: "userId") as? String {
            let appleIDProvider = ASAuthorizationAppleIDProvider()
            appleIDProvider.getCredentialState(forUserID: userID) { (state, error) in
                
                DispatchQueue.main.async {
                    switch state
                    {
                    case .authorized: // valid user id
                        self.settings.authorization = 1
                        break
                    case .revoked: // user revoked authorization
                        self.settings.authorization = -1
                        break
                    case .notFound: //not found
                        self.settings.authorization = 0
                        break
                    default:
                        break
                    }
                }
            }
        }
        
        let contentView = ContentView()
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))
            self.window = window
            window.makeKeyAndVisible()
        }
}

The following line in the above piece of code updates the EnvironmentObject of the ContentView that we saw earlier:

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))

We can then choose to show or hide the Sign in with Apple Button based on the latest authorization status present in the EnvironmentObject.

Here’s a screengrab of the SwiftUI application we’ve built:

swiftui-sign-in-with-apple

What’s Next

In this article, we mixed the SwiftUI state-driven framework with the Sign in with Apple authorization flow. Currently, once you’ve signed in and try signing again, the credentials instance won’t fetch the user name and email again.

In the next part, we’ll bring Keychain into the mix and see how it helps in retrieving the user info of already-existing users.

That’s it for this one. I hope you enjoyed reading it.

By Anupam Chugh

iOS Developer exploring the depths of ML and AR on Mobile.
Loves writing about thoughts, technology, and code.

Leave a Reply

Your email address will not be published. Required fields are marked *