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?
Sign In with Apple is a new feature in iOS 13 which allows for faster registration and authentication in your app.
While Apple repeatedly states that Sign In with Apple is straightforward to implement, there exist a few quirks to manage. In this tutorial, you’ll not only learn how to implement Sign In with Apple properly but also how to do so using SwiftUI!
You’ll need a copy of Xcode 11, a paid Apple Developer membership and an iOS 13 device to follow along with this tutorial.
Getting Started
Please download the materials for this tutorial using the Download Materials button found at the top or bottom of this tutorial. Because you’ll be running on a device and dealing with entitlements, set your team identifier and update the bundle identifier appropriately, by going to the Project navigator and click the new Signing & Capabilities tab. If you build and run the app right now, you’ll see a normal looking login screen:
Add Capabilities
Your provisioning profile needs to have the Sign In with Apple capability, so add it now. Click the project in the Project navigator, select the SignInWithApple target and then click the Signing & Capabilities tab. Finally, click + Capability and add the Sign In with Apple capability.
If your app has an associated website, you should also add the Associated Domains capability. This step is completely optional and not required for this tutorial, or to make Sign In with Apple function. If you do choose to use an associated domain, be sure to set the Domains value to the string webcredentials:
followed by the domain name. For example, you might use webcredentials:www.mydomain.com
. You’ll learn about the changes you need to make to your website later in the tutorial.
Add Sign In Button
Apple does not provide a SwiftUI View
for the Sign In with Apple button, so you need to wrap one yourself. Create a new file named SignInWithApple.swift and paste this code.
import SwiftUI
import AuthenticationServices
// 1
final class SignInWithApple: UIViewRepresentable {
// 2
func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
// 3
return ASAuthorizationAppleIDButton()
}
// 4
func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
}
}
Here’s what’s happening:
- You subclass
UIViewRepresentable
when you need to wrap aUIView
. -
makeUIView
should always return a specific type ofUIView
. - Since you’re not performing any customization, you return the Sign In with Apple object directly.
- Since the view’s state never changes, leave an empty implementation.
Now that you can add the button to SwiftUI, open ContentView.swift and add it just below the UserAndPassword
view:
SignInWithApple()
.frame(width: 280, height: 60)
Apple’s style guide calls out a minimum size of 280×60, so be sure to follow that. Build and run your app, and you should see your button!
Handle Button Taps
Right now, tapping the button does nothing. Just below where you set the frame
of the button, add a gesture recognizer:
.onTapGesture(perform: showAppleLogin)
And then implement showAppleLogin()
after the body
property:
private func showAppleLogin() {
// 1
let request = ASAuthorizationAppleIDProvider().createRequest()
// 2
request.requestedScopes = [.fullName, .email]
// 3
let controller = ASAuthorizationController(authorizationRequests: [request])
}
Here’s what you’ve set up:
- All sign in requests need an
ASAuthorizationAppleIDRequest
. - Specify the type of end user data you need to know.
- Generate the controller which will display the sign in dialog.
You should request only user data which you need. Apple generates a user ID for you. So, if your only purpose in grabbing an email is to have a unique identifier, you don’t truly need it — so don’t ask for it. ;]
ASAuthorizationControllerDelegate
When the user attempts to authenticate, Apple will call one of two delegate methods, so implement those now. Open SignInWithAppleDelegates.swift. You’ll implement the code here which runs after the user taps the button. While you could implement the code right in your view, it’s cleaner to place it elsewhere for reusability.
You’ll just leave the authorizationController(controller:didCompleteWithError:)
empty for now, but in a production app, you should handle these errors.
When authorization is successful, authorizationController(controller:didCompleteWithAuthorization:)
will be called. You can see in the downloaded sample code there are two cases you’ll want to handle. By examining the credential
property, you determine whether the user authenticated via Apple ID or a stored iCloud password.
The ASAuthorization
object passed to the delegate method includes any properties you asked for, such as the email or name. The existence of the value is how you can determine whether this is a new registration or an existing login.
The preceding note is critical to remember! Apple assumes that you’ll store the details you asked for and thus not require them again. This is one of the quirks of Sign In with Apple that you need to handle.
Consider the case where a user is signing in for the first time, so you need to perform registration. Apple hands you the user’s email and full name. Then, you attempt to call your server registration code. Except, your server isn’t online or the device’s network connection drops, etc.
The next time the user signs in, Apple won’t provide the details because it expects you already possess them. This causes your “existing user” flow to run, resulting in failure.
Handling Registration
In authorizationController(controller:didCompleteWithAuthorization:)
, just inside the first case
statement, add the following:
// 1
if let _ = appleIdCredential.email, let _ = appleIdCredential.fullName {
// 2
registerNewAccount(credential: appleIdCredential)
} else {
// 3
signInWithExistingAccount(credential: appleIdCredential)
}
In this code:
- If you receive details, you know it’s a new registration.
- Call your registration method once you receive details.
- Call your existing account method if you don’t receive details.
Paste the following registration method at the top of the extension:
private func registerNewAccount(credential: ASAuthorizationAppleIDCredential) {
// 1
let userData = UserData(email: credential.email!,
name: credential.fullName!,
identifier: credential.user)
// 2
let keychain = UserDataKeychain()
do {
try keychain.store(userData)
} catch {
self.signInSucceeded(false)
}
// 3
do {
let success = try WebApi.Register(
user: userData,
identityToken: credential.identityToken,
authorizationCode: credential.authorizationCode
)
self.signInSucceeded(success)
} catch {
self.signInSucceeded(false)
}
}
There are a few things occurring here:
- Save the desired details and the Apple-provided
user
in a struct. - Store the details into the iCloud keychain for later use.
- Make a call to your service and signify to the caller whether registration succeeded or not.
Notice the usage of credential.user
. This property contains the unique identifier that Apple assigned to the end-user. Utilize this value — not an email or login name — when you store this user on your server. The provided value will exactly match across all devices that the user owns. In addition, Apple will provide the user with the same value for all of the apps associated with your Team ID. Any app a user runs receives the same ID, meaning you already possess all their information on your server and don’t need to ask the user to provide it!
Your server’s database likely already stores some other identifier for the user. Simply add a new column to your user type table which holds the Apple-provided identifier. Your server-side code will then check that column first for a match. If not found, revert to your existing login or registration flows, such as using an email address or login name.
Depending on how your server handles security, you may or may not need to send the credential.identityToken
and credential.authorizationCode
. OAuth flows use those two pieces of data. OAuth setup is outside the scope of this tutorial.
To ensure proper storage in the keychain, edit UserDataKeychain.swift in CredentialStorage and update account
to have the bundle identifier for your app and then append any other text value. I like to append .Details
to the bundle identifier. What matters is that the account
property and bundle identifier do not exactly match, so the stored value is only used for the purpose for which you intend it.
Handling Existing Accounts
As previously explained, when an existing user logs into your app, Apple doesn’t provide the email and full name. Add this method right below the registration method in SignInWithAppleDelegates.swiftto handle this case:
private func signInWithExistingAccount(credential: ASAuthorizationAppleIDCredential) {
// You *should* have a fully registered account here. If you get back an error
// from your server that the account doesn't exist, you can look in the keychain
// for the credentials and rerun setup
// if (WebAPI.login(credential.user,
// credential.identityToken,
// credential.authorizationCode)) {
// ...
// }
self.signInSucceeded(true)
}
The code you place in this method will be very app-specific. If you receive a failure from your server telling you the user is not registered, you should query your keychain, using retrieve()
. With the details from the returned UserData
struct, you then re-attempt registration for the end user.
Username and Password
The other possibility, when using Sign In with Apple, is the end user will select credentials which are already stored in the iCloud keychain for the site. In the second case
statement for authorizationController(controller:didCompleteWithAuthorization:)
add the following line:
signInWithUserAndPassword(credential: passwordCredential)
And then just below signInWithExistingAccount(credential:)
implement the appropriate method:
private func signInWithUserAndPassword(credential: ASPasswordCredential) {
// if (WebAPI.login(credential.user, credential.password)) {
// ...
// }
self.signInSucceeded(true)
}
Again, your implementation will be app-specific. But, you’ll want to call your server login and pass along the username and password. If the server fails to know about the user, you’ll need to run a full registration flow as you don’t possess any details available from the keychain for their email and name.
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.
The Environment
A new concept with SwiftUI is the Environment. It’s an area wherein you can store data that needs to be available to many of your SwiftUI views. To some degree, you can think of it like dependency injection.
Environment Setup
Take a look at EnvironmentWindowKey.swift, and you’ll see the code necessary to store a value in the SwiftUI environment. It’s boilerplate code wherein you define the key to pass to the @Environment
property wrapper and the value to be stored. Note how, since a class type is being stored, it explicitly marks the reference as weak
to prevent a retain cycle.
ContentView Changes
Jump back to ContentView.swift and add another property to the top of ContentView
:
@Environment(\.window) var window: UIWindow?
iOS will automatically populate the window
variable with the value stored in the environment.
In performSignIn(using:)
, modify the constructor call to pass the window property:
appleSignInDelegates = SignInWithAppleDelegates(window: window) { success in
You’ll also want to tell ASAuthorizationController
to use your class for the presentationContextProvider
delegate, so add this code right after assigning the controller.delegate
:
controller.presentationContextProvider = appleSignInDelegates
Update the Delegate
Open SignInWithAppleDelegates.swift to handle the new property and constructor changes to the class. Replace the class definition, but not the extension with all the registration and delegate methods, with the following:
class SignInWithAppleDelegates: NSObject {
private let signInSucceeded: (Bool) -> Void
// 1
private weak var window: UIWindow!
// 2
init(window: UIWindow?, onSignedIn: @escaping (Bool) -> Void) {
// 3
self.window = window
self.signInSucceeded = onSignedIn
}
}
Just a few changes:
- Store a new weak reference to the window.
- Add the
UIWindow
parameter as the first argument to the initializer. - Store the passed-in value to the property.
Finally, implement the new delegate type:
extension SignInWithAppleDelegates:
ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController)
-> ASPresentationAnchor {
return self.window
}
}
The delegate just has a single method to implement that is expected to return the window, which shows the Sign In with Apple modal dialog.
Update the Scene
There’s just one piece left to getting the UIWindow
into the environment. Open SceneDelegate.swift and replace this line:
window.rootViewController = UIHostingController(rootView: ContentView())
With these lines:
// 1
let rootView = ContentView().environment(\.window, window)
// 2
window.rootViewController = UIHostingController(rootView: rootView)
Two small steps do it all:
- You create the
ContentView
and append the value of thewindow
variable. - You pass that
rootView
variable to theUIHostingController
instead of the old initializer.
The environment
method returns some View
which basically means it’s taking your ContentView
, shoving the value you pass into that view’s environment, and then returning the ContentView
back to you. Any SwiftUI View
which is presented from the ContentView
will now also hold that value in its environment.
If you create a new root view anywhere else, that root will not contain the environment values unless you explicitly pass them back in as well.
Logins Do not Scroll
One downside to Sign In with Apple to keep in mind is that the window that iOS displays will not scroll! For most users that won’t matter, but it’s important to note. As the owner of the site that my app uses, for example, I have numerous logins. Not only do I have the app login itself, but I’ve got a login for the SQL database, for the PHP admin site, etc.
If you’ve got too many logins, it’s possible end users won’t see what they actually need. Try to ensure that if you’re linking an app to a site that the site only has logins which will matter to the app. Don’t just bundle all your apps under a single domain.
Where to Go From Here?
You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
SignInWithAppleDelegates.swift currently returns a Boolean success, but you’ll likely want to use something more like Swift 5’s Result
type so that you can return not only data from your server, but also custom error types on failure. Please see our video course, What’s New in Swift 5: Types if you’re not familiar with the Result
type.
We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more