We all know Firebase. It is the easiest way for someone to get started with the Cloud without spending much time or money. Every developer would have considered or used Firebase at least once in any of their projects. As part of Firebase Auth, Google Sign-In is the easiest login method to implement since it comes with a pre-built UI. We sure do have other social logins as well, but Google Sign-In could be considered the easiest to integrate since Firebase is a Google product in the first place.
In this blog post, I will show you a Clean way to implement Google Sign-In in a SwiftUI app using the Firebase SDK. I say Clean since we will use the Clean Architecture principles of Robert C. Martin to ensure that we decouple all the app layers. We would also be using MVVM to perform this integration.
So without further ado, let’s begin!
1. Create a SwiftUI app
Set up a fresh SwiftUI app (or use an existing one).
2. Add Dependencies
The next step is to add the dependencies that we need for this implementation. You could use Swift Package Manager or Cocoapods for this, but for the purpose of this blog post I will use Swift Package Manager here.
- GoogleSignIn – https://github.com/google/GoogleSignIn-iOS
- Firebase SDK – https://github.com/firebase/firebase-ios-sdk
3. Create a Firebase project and add its configuration to the SwiftUI project
4. Create a GoogleSignInAuthenticator class
To implement sign-in with Google we will create a class called GoogleSignInAuthenticator
which would handle showing the pop-up for signing in with a Google account. We would need to be defining 2 methods in it for now: signIn()
and signOut()
.
This is what it looks like:
import SwiftUI
import GoogleSignIn
/// An observable class for authenticating via Google.
final class GoogleSignInAuthenticator: ObservableObject {
#if os(iOS)
private let clientID = "<replace firebase clientID here>"
#elseif os(macOS)
private let clientID = "<replace firebase clientID here>"
#endif
private lazy var configuration: GIDConfiguration = {
return GIDConfiguration(clientID: clientID)
}()
/// Signs in the user based upon the selected account.'
/// - note: Successful calls to this will return the `GIDGoogleUser`
func signIn() async throws -> GIDGoogleUser {
#if os(iOS)
guard let rootViewController = await UIApplication.shared.keyWindow?.rootViewController else {
throw BusinessErrors.clientError()
}
return try await withCheckedThrowingContinuation { continuation in
GIDSignIn.sharedInstance.signIn(
with: configuration,
presenting: rootViewController
) { user, error in
guard let user = user else {
AppLogger.error(String(describing: error))
continuation.resume(with: .failure(BusinessErrors.serverError()))
return
}
continuation.resume(with: .success(user))
}
}
#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
AppLogger.error("There is no presenting window!")
throw BusinessErrors.clientError()
}
return try await withCheckedThrowingContinuation { continuation in
GIDSignIn.sharedInstance.signIn(
with: configuration,
presenting: presentingWindow
) { user, error in
guard let user = user else {
AppLogger.error(String(describing: error))
continuation.resume(with: .failure(BusinessErrors.serverError()))
return
}
continuation.resume(with: .success(user))
}
}
#endif
}
/// Signs out the current user.
func signOut() {
GIDSignIn.sharedInstance.signOut()
}
}
The signIn()
method here presents the pop-up to log in with a Google account, and once the user does it successfully, we return the GIDGoogleUser
object. One thing to note here is that we have a slightly different implementation for iOS and macOS.
The signOut()
method is fairly simple since we are just invoking signOut()
on GIDSignIn
‘s singleton.
5. Create the AuthService protocol
Although we could just use GoogleSignInAuthenticator
directly in our view model, a cleaner way would be for us to decouple the authentication logic from the view model. Therefore we will add one more layer between GoogleSignInAuthenticator
and the view model, and we will ensure that the view model only knows about that. This would be an authentication service that handles all different sign-in methods (like Apple, Facebook, etc.). Also, the view model should reference the AuthService
protocol, not the concrete class that implements it.
import Foundation
import FirebaseAuth
protocol AuthService {
func restorePreviousSignIn() async -> AuthState
func signInWithGoogle() async -> AuthState
func signOut() -> Bool
}
/// An enumeration representing logged in status.
enum AuthState {
/// The user is logged in and is the associated value of this case.
case signedIn(User)
/// The user is logged out.
case signedOut
}
6. Create FirebaseAuthService that implements AuthService
Now that we know what methods the view model would need from the auth service let us create an auth service using Firebase.
import Foundation
import FirebaseAuth
import GoogleSignIn
class FirebaseAuthService: AuthService {
private var googleSignInAuthenticator: GoogleSignInAuthenticator {
return GoogleSignInAuthenticator()
}
func restorePreviousSignIn() async -> AuthState {
return await withCheckedContinuation { continuation in
if let currentUser = Auth.auth().currentUser {
AppLogger.debug("Restoring previous sign in")
continuation.resume(returning: .signedIn(currentUser))
} else {
AppLogger.debug("Could not restore previous sign in")
continuation.resume(returning: .signedOut)
}
}
}
func signInWithGoogle() async -> AuthState {
do {
let user: GIDGoogleUser = try await googleSignInAuthenticator.signIn()
let authentication = user.authentication
guard let idToken = authentication.idToken else {
return .signedOut
}
let credential = GoogleAuthProvider.credential(
withIDToken: idToken,
accessToken: authentication.accessToken
)
do {
let authDataResult = try await Auth.auth().signIn(with: credential)
let user = authDataResult.user
return .signedIn(user)
} catch {
AppLogger.error("Unexpectedly got error while signing in with firebase: \(error)")
return .signedOut
}
} catch {
AppLogger.error("Unexpectedly got error while signing in with google: \(error)")
return .signedOut
}
}
func signOut() -> Bool {
do {
googleSignInAuthenticator.signOut()
try Auth.auth().signOut()
return true
} catch {
AppLogger.error("Encountered error signing out: \(error).")
return false
}
}
}
You would have noticed that there is a restorePreviousSignIn()
method. This method is invoked when the app loads to see if we can restore the user session from the previous sign-in. If this succeeds, the app will display the authenticated view. If this fails, the app will display the sign in view.
7. Create the AuthenticationViewModel
It is time for us to create the AuthenticationViewModel
from where we would be invoking the AuthService
.
import Foundation
import FirebaseAuth
/// A class conforming to `ObservableObject` used to represent a user's authentication status.
final class AuthenticationViewModel: ObservableObject {
/// The user's log in status.
/// - note: This will publish updates when its value changes.
@Published var state: AuthState
private var authService: AuthService {
return FirebaseAuthService()
}
/// Creates an instance of this view model.
init() {
self.state = .signedOut
}
func restorePreviousSignIn() {
Task {
let state = await authService.restorePreviousSignIn()
await MainActor.run(body: { [weak self] in
self?.state = state
})
}
}
/// Signs the user in with Google
func signInWithGoogle() {
Task {
let state = await authService.signInWithGoogle()
await MainActor.run(body: { [weak self] in
self?.state = state
})
}
}
/// Signs the user out.
func signOut() {
if authService.signOut() {
self.state = .signedOut
}
}
}
We are making this view model a class that conforms to the ObservableObject
protocol. This allows us to set @Published
to the property state
(of type AuthState
) in the view model. Doing this would publish the changes in this property to wherever it is being used, which allows us to manipulate the view being displayed.
8. Setup the app’s root view
This is an important part of the app. Not only should we set the root view here, but also the app’s global settings.
When using Firebase, we would have to configure the Firebase project in the UIApplicationDelegate. However, in SwiftUI, we don’t have a UIApplicationDelegate. An official workaround for this is to use to @UIApplicationDelegateAdaptor
like below.
We also need to create an instance of the AuthenticationViewModel and set it as a @StateObject
. This allows the view model to maintain a global state. We also need to pass this view model as an environment object to the ContentView
.
When the ContentView
appears, we would have to try restoring the previous sign-in. Lastly, we would need to handle openUrl
by sending it to the GIDSignIn.sharedInstance.handle(url)
method.
import SwiftUI
import GoogleSignIn
import UIKit
import FirebaseCore
@main
struct BlogPostApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var authViewModel = AuthenticationViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authViewModel)
.onAppear {
authViewModel.restorePreviousSignIn()
}
.onOpenURL { url in
GIDSignIn.sharedInstance.handle(url)
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
FirebaseApp.configure()
return true
}
}
9. Create the ContentView and SignInView
This is the UI part of SwiftUI. If you have previous experience with UIKit, this might be one of the mind-blowing parts of this post to you, mainly because of how easy it is to write UI logic to display authenticated or unauthenticated views based on our state objects. The past 8 steps we have completed are the foundation that makes this possible.
The first thing to do here is to claim our environment variable, which we have passed from the BlogPostApp
to the ContentView
. The step after is to determine which view to display based on the state
property of the AuthenticationViewModel
. Here is how we do it:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var authViewModel: AuthenticationViewModel
var body: some View {
return Group {
switch authViewModel.state {
case let .signedIn(user):
Text("\(user.displayName) is Signed In")
case .signedOut:
SignInView()
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onAppear {
self.isNavigationBarHidden = true
}
}
}
Once our ContentView
is set up, we finally have our SignInView
to be created, which would invoke the signInWithGoogle()
method of our AuthenticationViewModel
. Here we again make use of GoogleSignInSwift
to create a GoogleSignInButtonViewModel
. We pass this view model to a view provided by GoogleSignInSwift
called GoogleSignInButton
.
import SwiftUI
import GoogleSignInSwift
struct SignInView: View {
@EnvironmentObject var authViewModel: AuthenticationViewModel
@ObservedObject var googleSignInButtonViewModel = GoogleSignInButtonViewModel()
init() {
googleSignInButtonViewModel.state = .normal
googleSignInButtonViewModel.scheme = .dark
googleSignInButtonViewModel.style = .standard
}
var body: some View {
VStack {
GoogleSignInButton(viewModel: googleSignInButtonViewModel) {
authViewModel.signInWithGoogle()
}
.padding()
Spacer()
}
}
}
10. Time to run our app
And that’s all! We have finally implemented Google Sign-In in our SwiftUI app using the Firebase SDK. We can also verify that the user has in fact been added in Firebase by checking the console.
Hope this was an informative blog post. See you again with another post.
Until then, bis bald!
One thought on “Implementing Google Sign-In in a SwiftUI app using the Firebase SDK”