DEV Community

Andrew Min
Andrew Min

Posted on • Originally published at andrewmin.info

Google Sign-In with SwiftUI

Originally posted on my blog

Overview

Google provides an easy SDK for integrating Google Sign-In with an iOS app with a helpful guide. However, the guide goes over code examples using UIKit, so I'll be showing how to use Google Sign-In with SwiftUI and manage state (This is not meant to replace the guide, this just shows examples of using SwiftUI).

Installation

The guide has pages on setting up GoogleSignIn with your project. To summarize, you'll need to:

  • Add the GoogleSignIn package to your project with CocoaPods (pod 'GoogleSignIn').
  • Go to Google's Developer Console to create a new project, then get your OAuth 2.0 Client ID credential.
  • Add an entry in "URL Types" under the "Info" tab on your project target in Xcode. The URL scheme is found in the developer console under "iOS URL scheme" (which is just your "Client ID" with the domains reversed). Adding the URL scheme allows the sign-in webpage to redirect back to your app.

Google Delegate

Google recommends implementing the GIDSignInDelegate to your AppDelegate, but instead, we'll create a separate GoogleDelegate which will make it easier to manage SwiftUI state. GoogleDelegate will also implement ObservableObject and contain a published property—signedIn. The view that handles signing in can automatically update by observing the signedIn property.

import GoogleSignIn

class GoogleDelegate: NSObject, GIDSignInDelegate, ObservableObject {

    @Published var signedIn: Bool = false

    ...
}

Next, we'll have to implement the sign(_:didSignInFor:withError:) method which is called when a user has signed in. The guide provides an example implementation, but we'll add a line that sets our signedIn property to true if the login was successful.

func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
    if let error = error {
        if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
            print("The user has not signed in before or they have since signed out.")
        } else {
            print("\(error.localizedDescription)")
        }
        return
    }

    // If the previous `error` is null, then the sign-in was succesful
    print("Successful sign-in!")
    signedIn = true
}

Note: if you decide to handle an error a different way than just printing it, be sure to also check that the error isn't GIDSignInErrorCode.canceled. If a user clicks the sign-in button then closes the sign-in page, sign(_:didSignInFor:withError:) will be called with a GIDSignInErrorCode.canceled error which you'll most likely want to ignore.

App & Scene Delegate

Now that GoogleDelegate is complete, we'll need to add some more setup to our AppDelegate and SceneDelegate.

First, inside our AppDelegate's application(_:didFinishLaunchingWithOptions: (the application entry point), we'll setup our:

  • Client ID - The client ID is found inside the developer console.
  • Delegate - Our GoogleDelegate class.
  • Scopes - Any additional scopes your application needs to request.
// GIDSignIn's delegate is a weak property, so we have to define our GoogleDelegate outside the function to prevent it from being deallocated.
let googleDelegate = GoogleDelegate()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    GIDSignIn.sharedInstance().clientID = "...googleusercontent.com"
    GIDSignIn.sharedInstance().delegate = googleDelegate
    GIDSignIn.sharedInstance().scopes = Constants.GS.scopes

    return true
}

We also have to implement application(_:open:options:) which is called when the sign-in page redirects back to our app.

func application(_ app: UIApplication, open url: URL, options [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
    return GIDSignIn.sharedInstance().handle(url)
}

Next, we have to add a few more things to the scene(_:willConnectTo:options:) method in SceneDelegate. First, we'll be setting our googleDelegate as an EnvironmentObject so that our views can easily access it. Second, we also need to set GIDSignIn.sharedInstance().presentingViewController. Normally, we would set the presentingViewController to whatever UIViewController has our sign-in view, but since we don't have any view controllers, we'll just set it to rootViewController.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    // Get the googleDelegate from AppDelegate
    let googleDelegate = (UIApplication.shared.delegate as! AppDelegate).googleDelegate

    // Add googleDelegate as an environment object
    let contentView = ContentView()
        .environmentObject(googleDelegate)

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)

        // Set presentingViewControll to rootViewController
        GIDSignIn.sharedInstance().presentingViewController = window.rootViewController

        self.window = window
        window.makeKeyAndVisible()
    }
}

The Sign-In Button

Finally, with all the setup out of the way, we can create our sign-in page. We'll design our page such that if the user isn't signed in, we show a simple sign-in button. If the user is signed in, we'll show their name and email along with a sign-out button. Note that the focus here is functionality, not a pretty UI.

For the sign-in button, Google provides the GIDSignInButton class which is already has a standard Google-look and handles the tap action. Since the button is a UIControl, we need to wrap it in a UIViewRepresentable to use it in our SwiftUI body.

import GoogleSignIn
import SwiftUI

struct SignInButton: UIViewRepresentable {

    func makeUIView(context: Context) -> GIDSignInButton {
        let button = GIDSignInButton()
        // Customize button here
        button.colorScheme = .light
        return button
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {}   
}

struct ContentView: View {

    @EnvironmentObject var googleDelegate: GoogleDelegate

    var body: some View {
        SignInButton()
    }
}

Alternatively, you can create your own button and call GIDSignIn.sharedInstance().signIn() when it is tapped.

var body: some View {
    Button(action: {
        GIDSignIn.sharedInstance().signIn()
    }) {
        Text("Sign In")
    }
}

Great, now our user can sign in. However, the whole point of GoogleDelegate was to update our view once the user is signed in, so let's show some of the user's information once they're signed in. The profile information can be found in GIDSignIn.sharedInstance().currentUser!.profile.

We also need a sign-out button. When it's pressed we call GIDSignIn.sharedInstance().signOut() and also set signedIn in our GoogleDelegate to false to show the sign-in button again.

// To use if/else in our body, we need to wrap the view in a Group
var body: some View {
    Group {
        if googleDelegate.signedIn {
            VStack {
                Text(GIDSignIn.sharedInstance().currentUser!.profile.name)
                Text(GIDSignIn.sharedInstance().currentUser!.profile.email)
                Button(action: {
                    GIDSignIn.sharedInstance().signOut()
                    googleDelegate.signedIn = false
                }) {
                    Text("Sign Out")
                }
            }
        } else {
            Button(action: {
                GIDSignIn.sharedInstance().signIn()
            }) {
                Text("Sign In")
            }
        }
    }
}

Now if we run our app, we'll be greeted with a sign-in button. When pressed, the Google Sign-In modal will pop up. Note that if you requested sensitive scopes (scopes that can access personal information), you'll see a warning that says "This app isn't verified". To get rid of this, you'll need to get your app verified. But for testing, you can just press "Advanced" in the bottom left then "Go To *App* (unsafe)".

After you sign-in, the modal should disappear and the view should now show your name and email address, along with a sign-out button. Pressing sign-out will refresh the view and show the sign-in button once again.

"Once the user is signed in, the view shows their name and email."

Once the user is signed in, the view shows their name and email.

Finally, in order to automatically sign in our user when they close and reopen the app, we need to call GIDSignIn.sharedInstance().restorePreviousSignIn() when the user opens the sign-in page.

var body: some View {
    Group {
        ...
    }
    .onAppear {
        GIDSignIn.sharedInstance().restorePreviousSignIn()
    }
}

Summary

Using SwiftUI's ObservableObject to manage state made it really easy for our view to automatically update when the user signs in or out. With UIKit you would have to manage messy NotificationCenter posts to update the UI when the user signs in or out.

Doing useful things with Google Sign-In such as making API calls or to authenticate the user to your own backend is out of the scope of this article, but Google provides many of their own guides.

Originally posted on my blog

Top comments (0)