NS

Sign In with Apple in SwiftUI

Let’s recreate “Sign in with Apple” Button for our app in SwiftUI. Before that, let us get some terms out of the way

SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Build user interfaces for any Apple device using just one set of tools and APIs.” — Apple’s official documentation

The best part is you can integrate your views with components from the UIKit, AppKit, and WatchKit frameworks. In short, SwiftUI has made building user interfaces straightforward.

“Sign In with Apple is the fast, easy, and more private way to sign into apps and websites using the Apple ID that you already have.” — Apple’s official documentation

Why Sign In with Apple is Important?

  1. Apple would soon make every developer use Sign in with Apple mandatory. In sum, you don’t have another option.
  2. Sign in with Apple is built from the ground up to respect user’s privacy and keep them in control of their personal information.
  3. Instead of using a social media account, or filling out forms and choosing another new password, just tap the Sign in with Apple button, review your information, and sign in quickly and securely with Face ID, Touch ID, or the device passcode.
  4. Sign in with Apple does not track or profile users as they favorite apps and websites, and Apple retains only the information that’s needed to make sure they can sign in and manage your account.

Without further wait, let us get on how you could implement Sign In with Apple in SwiftUI. Before you begin, you need to add “Sign In with Apple” Capability in Xcode. To do that,

  1. Click on your project
  2. Go to Signing and Capabilities
  3. Click “+” sign and search for “Sign In with Apple” and select it.

That’s it, we are ready to implement Sign In With Apple. Now lets’ look at the code.

SwiftUI does not provide an AppleIdButton so let’s just start there. We would need to wrap one up ourselves. Here, we are simply wrapping ASAuthorizationAppleIdButton, so we could use it in SwiftUI

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
            ASAuthorizationAppleIDButton()
    }
    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
    }
}

AppleIdButton Wrapper for SwiftUI

At this point, in your ContentView, you should be able to render the official Apple Button like this,

Button (action: {}) {  
  AppleIdButton()  
}**//May be style it a little** AppleIdButton().background(Color.primary).clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)).padding(7).frame(width: UIScreen.main.bounds.width - 100, height: 65)

Let’s go back to AuthenticationService. Make sure you have AuthenticationServices imported. It provides a framework for handling Authorization requests. Next, we need to work on the Coordinator. This coordinator provides a mechanism to request Apple to register a new user, or sign in an existing user.


    func getApppleRequest() {
        let appleIdProvider = ASAuthorizationAppleIDProvider()
        let request = appleIdProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        let authController = ASAuthorizationController(authorizationRequests: [request])
        authController.delegate = self
        authController.performRequests()
    }
    
    private func setUserInfo(for userId: String, fullName: String?, email: String?) {
        ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userId) { credentialState, error in
            var authState: String?
            
            switch credentialState {
                case .authorized: authState = "authorized"
                case .revoked: authState = "revoked"
                case .notFound: authState = "notFound"
                case .transferred: authState = "transferred"
                @unknown default:
                        fatalError()
                }
            
            let user = User(fullName: fullName ?? "not provided", email: email ?? "email", authState: authState ?? "unknown")
            if let userEncoded = try? JSONEncoder().encode(user) {
                UserDefaults.standard.set(userEncoded, forKey: "user")
            }
        }
    }
}

extension SignInWithAppleCoordinator: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            let fullName = (credential.fullName?.givenName ?? "")
            setUserInfo(for: credential.user, fullName: fullName, email: credential.email)
        }
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print("Sign In with Apple ID Error: \(error.localizedDescription)")
    }
}

Lets look at this code closely:

  1. First, we create a request with AppleIDProvider and we request the user’s full name and email. One thing to note here is that this is optional and you can choose if you want to store this data or not.
  2. Next, we initialize the ASAuthorizationController with the request we build in the first step and execute our request for user registration or sign in.
  3. We need access to ASAuthorizationControllerDelegate protocol to be able to have access to delegate function to fetch verification (success or failure here)
  4. didCompleteWithAuthorization: Once the user is successfully signed in, it checks is the credential is ASAuthorizationAppleIdCredential. If yes, this function returns the name and email of the user. While you should be storing this is in the Keychain (more secure), we are just storing it in UserDefaults.
  5. didCompleteWithError: It is important to handle this case, where the sign-in fails and we return a localized description of the error.

If you noticed that we have saved the user in our UserDefault. Therefore, before getting here, we should check if the user is already signed in. This will keep user logged in until they choose to sign out manually. To achieve this, we will write a wrapper on top of getAppleRequest() to check if the user is already logged in.

**func** getUserInfo() {  
   **if** **let** userData = UserDefaults.standard.object(forKey: "user")                **as**? Data,  
   **let** userDecoded = **try**? JSONDecoder().decode(User.**self**, from:  userData) {  
      print("UserData: \\(userDecoded)")  
      user = userDecoded  
   }  
}

Let’s clean up our code a little by creating a ViewModel that our ContentView can communicate with:

    var signInWithApple = SignInWithAppleCoordinator()
    
    @Published var user: User?
    
    func getRequest() {
        signInWithApple.getApppleRequest()
    }
    
    func getUserInfo() {
        if let userData = UserDefaults.standard.object(forKey: "user") as? Data,
            let userDecoded = try? JSONDecoder().decode(User.self, from: userData) {
            print("UserData: \(userDecoded)")
            user = userDecoded
        }
    }
}

Finally, from our View, we create an AppleIdButton. With this, you should be able to route the user to your app if the user is already logged in.

  @ObservedObject var appleView = AppleViewModel()
  var body: some View {
    VStack {
      if appleView.user != nil {
        //User Logged In
      } else {
        Button(action: {
            self.appleView.getRequest()
        }){
            AppleIdButton()
                .background(Color.primary).clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)).padding(7)
                .frame(width: UIScreen.main.bounds.width - 100, height: 65)
        }
      }.onAppear( perform: {
          self.appleView.getUserInfo()
      })
    }
}

That’s it! Now you have “Sign In with Apple” built into your app.

Thanks for reading!