Sign in with Apple Using SwiftUI
Learn how to implement Sign in with Apple using SwiftUI, to give users more privacy and control in your iOS apps. By Scott Grosch.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sign in with Apple Using SwiftUI
25 mins
- Getting Started
- Add Capabilities
- Add Sign In Button
- Handle Button Taps
- ASAuthorizationControllerDelegate
- Handling Registration
- Handling Existing Accounts
- Username and Password
- Finish Handling Button Press
- Automate Sign In
- Web Credentials
- Runtime Checks
- The UIWindow
- The Environment
- Environment Setup
- ContentView Changes
- Update the Delegate
- Update the Scene
- Logins Do not Scroll
- Where to Go From Here?
Finish Handling Button Press
Back in ContentView.swift, you’ll need a property to store the delegate you just created. At the top of the class, add this line of code:
@State var appleSignInDelegates: SignInWithAppleDelegates! = nil
@State
is how you tell SwiftUI that your struct will have mutable content which it owns and updates. All @State
properties must possess an actual value, which is why the odd looking assignment to nil
is present.
Now, in the same file, finish off showAppleLogin()
by replacing the controller
creation with this:
// 1
appleSignInDelegates = SignInWithAppleDelegates() { success in
if success {
// update UI
} else {
// show the user an error
}
}
// 2
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = appleSignInDelegates
// 3
controller.performRequests()
Here’s what is happening:
- Generate the delegate and assign it to the class’ property.
- Generate the
ASAuthorizationController
as before, but this time, tell it to use your custom delegate class. - By calling
performRequests()
, you’re asking iOS to display the Sign In with Apple modal view.
The callback of your delegate is where you handle whatever presentation changes are necessary based on whether the end user successfully authenticated with your app.
Automate Sign In
You’ve implemented Sign In with Apple, but the user has to tap on the button explicitly. If you’ve taken them to the login page, you should see if they already configured Sign In with Apple. Back in ContentView.swift, add this line to the .onAppear
block:
self.performExistingAccountSetupFlows()
.onAppear { }
is essentially the same thing as UIKit’s viewDidAppear(_:)
.When the view appears, you want iOS to check both the Apple ID and the iCloud keychain for credentials that relate to this app. If they exist, you will automatically show the Sign In with Apple dialog, so the user doesn’t have to press the button manually. Since the button press and the automatic call with both share code, refactor your showAppleLogin
method into two methods:
private func showAppleLogin() {
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
performSignIn(using: [request])
}
private func performSignIn(using requests: [ASAuthorizationRequest]) {
appleSignInDelegates = SignInWithAppleDelegates() { success in
if success {
// update UI
} else {
// show the user an error
}
}
let controller = ASAuthorizationController(authorizationRequests: requests)
controller.delegate = appleSignInDelegates
controller.performRequests()
}
There are no code changes other than moving the delegate creation and display into a method of its own.
Now, implement performExistingAccountSetupFlows()
:
private func performExistingAccountSetupFlows() {
// 1
#if !targetEnvironment(simulator)
// 2
let requests = [
ASAuthorizationAppleIDProvider().createRequest(),
ASAuthorizationPasswordProvider().createRequest()
]
// 2
performSignIn(using: requests)
#endif
}
There are only a couple of steps here:
- If you’re using the simulator, do nothing. The simulator will print out an error if you make these calls.
- Ask Apple to make requests for both Apple ID and iCloud keychain checks.
- Call your existing setup code.
Notice how, in step 2, you didn’t specify what end-user details you wanted to retrieve. Recall earlier in the tutorial, where you learned that the details would only be provided a single time. Since this flow is used to check existing accounts, there’s no reason to specify the requestedScopes
property. In fact, if you did set it here, it would simply be ignored!
Web Credentials
If you have a website dedicated to your app, you can go a little further and handle web credentials as well. If you take a peek in UserAndPassword.swift, you’ll see a call to SharedWebCredential(domain:)
, which currently sends an empty string to the constructor. Replace that with the domain of your website.
Now, log into your website and at the root of the site create a directory called .well-known. In there, create a new file called apple-app-site-association and paste in the following JSON:
{
"webcredentials": {
"apps": [ "ABCDEFGHIJ.com.raywenderlich.SignInWithApple" ]
}
}
You’ll want to replace the ABCDEFGHIJ
with your team’s 10-character Team ID. You can find your Team ID at https://developer.apple.com/account under the Membership tab. You’ll also need to make the bundle identifier match whatever you’re using for the app.
By taking those steps, you’ve linked Safari’s stored login details with your app’s login details. They will now be available for Sign in with Apple.
When the user manually enters a username and password the credentials will be stored so that they’re available for later use.
Runtime Checks
At any point during the lifetime of your app, the user can go into device settings and disable Sign In with Apple for your app. You’ll want to check, depending on the action to be performed, whether or not they are still signed in. Apple recommends you run this code:
let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: "currentUserIdentifier") { state, error in
switch state {
case .authorized:
// Credentials are valid.
break
case .revoked:
// Credential revoked, log them out
break
case .notFound:
// Credentials not found, show login UI
break
}
}
Apple has said that the getCredentialState(forUserId:)
call is extremely fast. So you should run it during app startup and any time you need to ensure the user is still authenticated. I recommend you not run at app startup unless you must. Does your app really require a logged in or registered user for everything? Don’t require them to log in until they try to perform an action that actually requires being signed in. In fact, even the Human Interface Guidelines recommend this too!
Remember that many users will uninstall a just downloaded app if the first thing they are asked to do is register.
Instead, listen to the notification that Apple provides to know when a user has logged out. Simply listen for the ASAuthorizationAppleIDProvider.credentialRevokedNotification
notification and take appropriate action.
The UIWindow
At this point, you’ve fully implemented Sign In with Apple. Congratulations!
If you watched the WWDC presentation on Sign In with Apple or have read other tutorials, you might notice that there’s a piece missing here. You never implemented the ASAuthorizationControllerPresentationContextProviding
delegate method to tell iOS which UIWindow
to use. While technically not required if you’re using the default UIWindow
, it’s good to know how to handle.
If you’re not using SwiftUI, it’s pretty simple to grab the window
property from your SceneDelegate
and return the value. In SwiftUI, it becomes much harder.