Category Archives: Swift

Adding Google sign-in in iOS with SwiftUI

 

 Git Hub Repo

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

Covered here

Short cheatsheet below:

  1. If you don’t already have CocoaPods installed, follow the steps in the CocoaPods Getting Started guide.
  2. Open a terminal window and navigate to the location of your app’s Xcode project.
  3. If you have not already created a Podfile for your application, create one now:
    pod init
  4. Open the Podfile created for your application and add the following:
    pod 'GoogleSignIn'
  5. If you are using SwiftUI, also add the pod extension for the “Sign in with Google” button:
    pod 'GoogleSignInSwiftSupport'
  6. Save the file and run:
    pod install
  7. 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.)
  8. 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

Google article

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";

 

Using EnvironmentObject to share data between views

//
//  ContentView.swift
//  Test
//
//  Created by Toni Nichev on 1/3/24.
//

import SwiftUI


// Our observable object class
class GameSettings: ObservableObject {
    @Published var scoree = 0
    var test = 4
}

// A view that expects to find a GameSettings object
// in the environment, and shows its score.
struct ScoreView: View {
    // 2: We are not instantiating gameSetting here since it's already done in ContentView. 
    @EnvironmentObject var gameSettings: GameSettings
    
    var body: some View {
        Text("Score: \(gameSettings.scoree)")
        Text("Test: \(gameSettings.test)")
    }
}

struct ContentView: View {
    // 1: We instantiate GameSettings only here and pass it to the environmentObject at the end
    @StateObject var gameSettings = GameSettings()
    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                
                Button("Increase score") {
                    gameSettings.scoree += 1
                    gameSettings.test += 1
                }
                
                NavigationLink {
                    ScoreView()
                } label: {
                    Text("Show score")
                }
            }
        }
        .environmentObject(gameSettings)
    }
}

#Preview {
    ContentView()
}

#Preview {
    ScoreView().environmentObject(GameSettings())
}