Table of Contents
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
.xcworkspaceworkspace file for your application in Xcode. Use this file for all future development on your application. (Note that this is different from the included.xcodeprojproject 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";
