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