Google also has a great tutorial here: Get started with Google Sign-In for iOS and macOS
Pre-requirements:
Create authorization credentials.
This is covered in another article here.
Install GoogleSignIn and GoogleSignInWithSwiftSupport
Short cheatsheet below:
- If you don’t already have CocoaPods installed, follow the steps in the CocoaPods Getting Started guide.
- Open a terminal window and navigate to the location of your app’s Xcode project.
- If you have not already created a Podfile for your application, create one now:
pod init
- Open the Podfile created for your application and add the following:
pod 'GoogleSignIn'
- If you are using SwiftUI, also add the pod extension for the “Sign in with Google” button:
pod 'GoogleSignInSwiftSupport'
- Save the file and run:
pod install
- From now on Open the generated
.xcworkspace
workspace file for your application in Xcode. Use this file for all future development on your application. (Note that this is different from the included.xcodeproj
project file, which would result in build errors when opened.) - Now we are almost ready to start coding, but when we build the project we might (or might not depends of X-code version) face some issues..
Fixing error rsync.samba(4644) deny(1) file-write-create
Navigate to the Build Settings, find ‘User Script Sandboxing’ and
Flip it to No
Fixing “Your app is missing support for the following URL schemes:”
Copy missing scheme from the error message and add it in the info->url section
Let’s get started
Adding Google Client ID (GIDClientID)
Ether you face the problems before or not this is one thing that is mandatory.
1. Adding UserAuthModel to share between all views.
If you don’t know how to do this read about ObservableObject
and @Published
and sharing data between Views.
This class has to conform to the ObservableObject
in order to have its properties reflecting the View.
We will create methods to check if user is signed in, and update shared parameters: givenName, userEmail, isLoggedIn …
import SwiftUI import GoogleSignIn import GoogleSignInSwift final class UserAuthModel: ObservableObject { @Published var givenName: String = "" @Published var isLoggedIn: Bool = false @Published var errorMessage: String = "" @Published var userEmail: String = "" @Published var profilePicUrl: String = "" init() { check() } func getUserStatus() { if GIDSignIn.sharedInstance.currentUser != nil { let user = GIDSignIn.sharedInstance.currentUser guard let user = user else { return } let givenName = user.profile?.givenName self.givenName = givenName ?? "" self.userEmail = user.profile!.email self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString self.isLoggedIn = true } else { self.isLoggedIn = false self.givenName = "Not Logged In" } } func check() { GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in if let error = error { self.errorMessage = "error: \(error.localizedDescription)" } self.getUserStatus() } } func gertRootViewController() -> UIViewController { guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return .init() } guard let root = screen.windows.first?.rootViewController else { return .init() } return root } func signIn() { GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in guard let result = signInResult else { // Inspect error print("Error occured in signIn()") return } print("Signing in ...") print(result.user.profile?.givenName ?? "") self.getUserStatus() } } func signOut() { GIDSignIn.sharedInstance.signOut() self.getUserStatus() }
Now let’s edit the app starter and put userAuthModel in the environmentObject
// // SignInWithGoogleTutorialApp.swift // SignInWithGoogleTutorial // // Created by Toni Nichev on 1/3/24. // import SwiftUI @main struct SignInWithGoogleTutorialApp: App { @StateObject var userAuthModel: UserAuthModel = UserAuthModel() var body: some Scene { WindowGroup { NavigationView { ContentView() } .environmentObject(userAuthModel) } } }
Adding Sign In / Sign Out buttons to the View
// // ContentView.swift // SignInWithGoogleTutorial // // Created by Toni Nichev on 1/3/24. // import SwiftUI struct ContentView: View { @EnvironmentObject var userAuthModel: UserAuthModel fileprivate func signInButton() -> some View { HStack { Image("GoogleSignInButton") .resizable() .frame(width: 50, height: 50) Button(action: { userAuthModel.signIn() }, label: { Text("Sign In") }) } } fileprivate func signOutButton() -> Button<Text> { Button(action: { userAuthModel.signOut() }, label: { Text("Sign Out") }) } fileprivate func profilePic() -> some View { AsyncImage(url: URL(string: userAuthModel.profilePicUrl)) .frame(width: 100,height: 100) } var body: some View { VStack { if userAuthModel.isLoggedIn { profilePic() Text("Hello: \(userAuthModel.givenName)") signOutButton() } else { signInButton() } } } } #Preview { ContentView().environmentObject(UserAuthModel()) }
We have to also edit the #Preview and add userAuthModel there so the preview won’t break.
Adding Authentication with a backend server
The purpose of authentication on the backend server is to make sure that logged-in users could have access to some protected content, like subscriptions, pro-articles, etc.
Once the user signs-in in the native app, the app sends the id-token to the backend, and the backend validates the token and could return access-token back to the app.
In the previous chapter we added UserAuthModel.swift file.
This is the place to call the backend server.
func sendTokenToBackendServer() { let user = GIDSignIn.sharedInstance.currentUser guard let user = user else { return } let stringToken = user.idToken!.tokenString guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else { return } let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in print(response ?? ".") // handle response from my backend. if error != nil { print("Error: \(String(describing: error))") } // Handle the response from the server let dataString = String(data: data!, encoding: .utf8) print ("got data: \(dataString!)") } task.resume() }
and the final UserAuthenticationModel.swift will look like this:
import SwiftUI import GoogleSignIn import GoogleSignInSwift final class UserAuthModel: ObservableObject { @Published var givenName: String = "" @Published var isLoggedIn: Bool = false @Published var errorMessage: String = "" @Published var userEmail: String = "" @Published var profilePicUrl: String = "" init() { check() } func getUserStatus() { if GIDSignIn.sharedInstance.currentUser != nil { let user = GIDSignIn.sharedInstance.currentUser guard let user = user else { return } let givenName = user.profile?.givenName self.givenName = givenName ?? "" self.userEmail = user.profile!.email self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString self.isLoggedIn = true } else { self.isLoggedIn = false self.givenName = "Not Logged In" } } func check() { GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in if let error = error { self.errorMessage = "error: \(error.localizedDescription)" } self.getUserStatus() } } func gertRootViewController() -> UIViewController { guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return .init() } guard let root = screen.windows.first?.rootViewController else { return .init() } return root } func signIn() { GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in guard let result = signInResult else { // Inspect error print("Error occured in signIn()") return } print("Signing in ...") print(result.user.profile?.givenName ?? "") self.getUserStatus() self.sendTokenToBackendServer() } } func signOut() { GIDSignIn.sharedInstance.signOut() self.getUserStatus() } func sendTokenToBackendServer() { let user = GIDSignIn.sharedInstance.currentUser guard let user = user else { return } let stringToken = user.idToken!.tokenString guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else { return } let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in print(response ?? ".") // handle response from my backend. if error != nil { print("Error: \(String(describing: error))") } // Handle the response from the server let dataString = String(data: data!, encoding: .utf8) print ("got data: \(dataString!)") } task.resume() } }
Server script to get idToken form the native app:
In the example below we Just save the token to a file. In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app.
<?php // SAVE RAW DATA $appleData = file_get_contents('php://input'); // Just saves the token to a file. // In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app $file = fopen("./data.txt", "a"); fwrite($file, $appleData); fclose($file); echo "send something back to the native app like acccess-token";