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.
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)