Chapters

Hide chapters

watchOS With SwiftUI by Tutorials

First Edition · watchOS 8 · Swift 5.5 · Xcode 13.1

Section I: watchOS With SwiftUI

Section 1: 16 chapters
Show chapters Hide chapters

14. Sign in With Apple
Written by Scott Grosch

Sign in with Apple, or SIWA, has been around for a few years. Your customers will greatly appreciate the simplicity of registration and authentication when using a device with a screen as small as the Apple Watch.

You should consider this a bonus chapter, as there’s nothing Apple Watch-specific related to SIWA. If you’re already familiar with how to implement SIWA, feel free to skip this chapter. Due to the extra importance of a streamlined experience on the Apple Watch, I felt it made sense to include this content in the book.

Note: To complete this chapter, you’ll need a paid Apple developer account to set up the required identifiers and profiles.

This chapter only deals with the watchOS parts of SIWA. Embedded Sign in with Apple JS, web server and developer portal configurations are outside the scope of this book.

Some vendors won’t implement SIWA due to the anonymization of email addresses. When a customer emails the vendor with a support ticket, it’s harder to locate the customer’s account. However, you should keep in mind that your user’s experience is what you should prioritize, not yours.

To authenticate or not

Build and run the SignIn app from this chapter’s starter materials. You’ll see a simple screen that lets you choose which of two views to display. Tap the top button, and you’re taken to the part of the app which doesn’t require authentication. However, tap the second button, and you’re asked to authenticate:

Authenticate via username and password

The authentication screen you currently see is what most apps present: a simple username and password entry view.

Take a look at Authenticating/PasswordView.swift. For the Apple Watch, it’s especially important to specify the text content type:

TextField("User Name", text: $userName)
  .textContentType(.username)

SecureField("Password", text: $password)
  .textContentType(.password)

Specifying the content type gives watchOS clues about how to present and handle the data. A .username or .password field can be appropriately linked to the keychain, for example.

Next, notice isButtonDisabled computed property:

private var isButtonDisabled: Bool {
  return userName.trimmingCharacters(in: .whitespaces).isEmpty
    || password.isEmpty
}

As well as its use on the button:

Button("Sign In", action: signInTapped)
  .disabled(isButtonDisabled)

There’s no point in letting the user tap the Sign In button if they haven’t properly supplied all required fields.

Note: Instead of disabling the button, you could also present an alert sheet when the user provides invalid data.

Finally, notice how self-contained the view is. It performs a single task, which is to gather the input. When your user taps the Sign In button, something happens, but this view doesn’t care what. It simply delegates the work elsewhere by passing the details to the completionHandler.

Handling sign in

Now switch to Authenticating/SignInView.swift. The SignInView is where you handle the how of your app’s login. The first piece you’ll notice is the use of app storage:

@AppStorage("userName") private var storedUserName = ""

Tracking the username isn’t a detail of PasswordView because it shouldn’t care where the username comes from. While app storage will be a common location, you might just as easily store the user’s information in Core Data.

The completion handler for the PasswordView performs your actual authentication.

private func userPasswordCompletion(userName: String, password: String) {
  // 1
  storedUserName = userName
  // 2
  var request = URLRequest(
    url: URL(string: "https://your.site.com/login")!
  )
  request.httpMethod = "POST"
  // request.httpBody = ...

  DispatchQueue.main.async {
    // 3
    onSignedIn("some token here")
    // 4
    dismiss()
  }
}

Here, the code:

  1. Updates the username you’re storing with whatever value the user supplied. If they change usernames, they likely want to use the same one next time.
  2. Generates your URLRequest as appropriate to send the username and password to your web service.
  3. Once the authentication successfully returns, it calls the completion handler supplied to SignInView. The example above assumes your web service returns some type of token to use for future network calls.
  4. Normally, this view is pushed onto a stack or displayed in a modal dialog. If authentication was successful, it dismisses the view.

Your app may send a Decodable object back, it might supply values in a header or any other combination of responses. You’ll need to update the code as appropriate. Keep three key details in mind.

  • Call the completion handler with the appropriate response data.
  • Dismiss the view if authentication was successful.
  • Ensure you’re working against the main thread when calling your completion handler, dismissing the view and presenting any errors.

Signing in with Apple

At this point, your login screen is fully functional. Functional, but not friendly. I don’t know about you, but I always struggle to draw letters exactly as the Apple Watch wants them. Wouldn’t it be better to have a Sign in with Apple button available?

Time to finally do some coding!

Adding the SIWA button

While still editing SignInView.swift, add an import to the top of the file:

import AuthenticationServices

AuthenticationServices provides the code necessary for SIWA.

Now add two stub methods to the SignInView in this file:

private func onRequest(request: ASAuthorizationAppleIDRequest) {
}

private func siwaCompletion(result: Result<ASAuthorization, Error>) {
}

You’ll populate both methods in a moment, but they’re required for the button to be used inside body. Now place the following code between the Text and PasswordView elements in the body:

SignInWithAppleButton(
  onRequest: onRequest,
  onCompletion: siwaCompletion
)
  .signInWithAppleButtonStyle(.white)

Divider()
  .padding()

Take a look in the Canvas, inside of Xcode, and you’ll now see your shiny new SIWA button:

That’s all it takes to place the button. Now that SIWA has full SwiftUI support, you no longer need to implement a UIViewRepresentable yourself. Nice.

Configuring the request

You’ll configure the request in the onRequest(request:) stub you generated. Add the following code there:

request.requestedScopes = [.fullName]
request.state = "some state string"

You told Apple that you need to know the full name of the person who is authenticating. requestedScopes allows for a .email option as well, but you should only request it if you need it. Keep in mind that most users will hide their email from you and use Apple’s relay service. Apple will provide you a unique identifier to represent the user in your database, so don’t ask for the email just so you have a unique “key” to represent the user.

state returns to you unmodified in the credential Apple supplies. You can provide whatever user-defined state you wish as it’s simply echoed back to you in Apple’s response.

Optionally, you may use nonce to mitigate replay attacks. A replay attack occurs when a malicious user copies and resends a data packet to you. By using a unique nonce value in every request, you ensure that Apple sent the packet you received. At the top of SignInView, add a private property:

private let nonce = UUID().uuidString

Then, inside onRequest(request:) set the request’s nonce:

request.nonce = nonce

Note: Decoding and validating the nonce is outside the scope of this chapter.

Handling the reply

When you get a reply back from Apple, there are three possible outcomes:

  1. The authentication failed.
  2. This is the first time successfully authenticating against your app.
  3. You successfully authenticated, and it’s not the first time.

To handle the first case, add the following code to siwaCompletion(result:):

guard
  // 1
  case .success(let authorization) = result,
  // 2
  let credential = authorization.credential as? ASAuthorizationAppleIDCredential
else {
  // 3
  if case .failure(let error) = result {
    print("Failed to authenticate: \(error.localizedDescription)")
  }

  return
}

In the preceding code:

  1. First, you store the authorization returned from Apple on success.
  2. Next, you retrieve the ASAuthorizationAppleIDCredential.
  3. If either of the first two steps fails and an error returns, display it. Your production code should, of course, display a message to the user as opposed to just printing to the console.

If a failure didn’t occur, then you handle either login or registration. If fullName, which you requested in onRequest(request:) via requestedScopes, contains a value, then this is the first time you’ve authenticated this user.

If fullName is nil, then you’ve authenticated an existing user. It’s unfortunate, but Apple will only return the user’s full name and email address the first time. It’s your responsibility to save that data in an app-appropriate way.

In your method, you’ll simply check the property’s value. Add the following code after the guard block you just added:

if credential.fullName == nil {
  // You've logged in an existing user account.
} else {
  // The user does not exist, so register a new account.
}

Depending on your app’s configuration, you’ll then need to take appropriate action. Usually, that means making a call to your web server to generate a new account or sign in to an existing account. In either case, you’ll likely want to pass the credential.user, credential.identityToken and credential.authorizationCode to your server.

Note: Remember to call onSignedIn() after contacting your web server.

There’s a critical component of new user registration to keep in mind. Apple will only provide the name and email address the first time you authenticate. If the registration call you make to your web server fails for any reason, you have to have a way to try again. Consider a scenario where your web server crashes or the Apple Watch loses network connectivity.

You should use some form of secure storage to store the data you need for registration until your server successfully registers the user. You can use the KeyChain, Core Data or any other secure storage mechanism that doesn’t require the network.

Storing the user credentials

The user property of the credential contains the unique identifier that Apple assigned to your user. You’ll likely want to pass that identifier to all the calls you send to your web server. A simple way to handle that requirement is to add another @AppStorage to the top of SignInView:

@AppStorage("userCredential") private var userCredential = ""

And then, at the end of siwaCompletion(result:), assign the value:

userCredential = credential.user

Storing their name properly

If you’ve requested the user’s full name, keep in mind that the value provided is a PersonNameComponents, not a String. To display the name, use the appropriate formatter, like so:

PersonNameComponentsFormatter.localizedString(
  from: name,
  style: .default
)

Different cultures display names differently, so you should never assume it’s safe to send something like this to your server:

let me = credential.fullName
let badNameExample = "\(me.givenName) \(me.familyName)"

You may be tempted to use the formatter and then send that value to the web server. Don’t do that either! Users from one country may see names of users from another country, so you want to ensure each person sees the name formatted correctly for their locale.

PersonNameComponents is Codable, meaning it’s incredibly simple to send to your web server:

let encoder = JSONEncoder()
guard let data = try? encoder.encode(credential.fullName) else {
  // handle error appropriately
  return
}

let encoded = data.base64EncodedString()

Send the encoded value to your web server for storage. When you retrieve the name from your web server, perform the operations in reverse:

let decoder = JSONDecoder()

guard
  let data = Data(base64Encoded: encoded),
  let components = try? decoder.decode(
    PersonNameComponents.self,
    from: data
  )
else {
  // handle error appropriately
  return
}

let name = PersonNameComponentsFormatter.localizedString(
  from: components,
  style: .short
)

Key points

  • Adding SIWA is relatively easy, so use it wherever possible.
  • Ensure that you store new user registration details local to the device until your app’s web server performs a successful registration.
  • Only request the user’s name and email address if you truly need them.
  • Keep in mind that most email addresses you receive will be an Apple relay address.
  • Always store the full PersonNameComponentsFormatter details on your web server.

Where to go from here?

For full details on SIWA, please see Apple’s document, Sign in with Apple.

Vapor is a great server-side option when using Sign in with Apple. See our book, Server-Side Swift with Vapor, for complete implementation details.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.