Google Sign-In with SwiftUI
Tags: swift, tutorial, ios
Published: 2020-06-29 04:32:59

*Originally posted on my [blog](https://www.andrewmin.info/blog/google-sign-in/)*

# Overview

Google provides an easy SDK for integrating Google Sign-In with an iOS app with a helpful [guide](https://developers.google.com/identity/sign-in/ios/start-integrating). 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](https://developers.google.com/identity/sign-in/ios/start-integrating) 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](https://console.developers.google.com) 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.

```swift
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.

```swift
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](https://developers.google.com/identity/protocols/oauth2/scopes) your application needs to request.

```swift
// 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.

```swift
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`.

```swift
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.

```swift
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.

```swift
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.

```swift
// 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](https://support.google.com/cloud/answer/7454865?hl=en). 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."](https://www.andrewmin.info/assets/images/google-sign-in/signed-in.png)
<figcaption>Once the user is signed in, the view shows their name and email.</figcaption>

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.

```swift
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](https://www.andrewmin.info/blog/google-sign-in/)*