(SwiftUI) Utilizing SwiftUI for Google and Facebook Login with Firebase Authentication

這篇文章主要紀錄了我如何在 SwiftUI 中,透過 Google、Facebook 以及Firebase 中註冊的 Email 實現 Firebase 身份驗證

目錄:
FireBase 基本設置教學
實現 Firebase 登入以及註冊的功能
實現 Google 登入的功能
實現 Facebook 登入的功能
判斷是否已經登入
需要注意的地方

環境:

firebase-ios-sdk 10.17.0

facebook-ios-sdk 13.2.0

googlesignin-ios 6.2.4

FireBase 基本設置教學

簡單設計一下登入的畫面

要實現多個登入功能的話,可以將登入的方法,寫在一個 Swift File 中 AuthenticationViewModel

在裡面你可以定義多個 enum 來紀錄你的狀態,還有把存登入帳號所使用的 Email 或是 Photo

import Foundation
import Firebase
import GoogleSignIn
import FirebaseAuth
import FacebookLogin

class AuthenticationViewModel: ObservableObject {

// 定義 Firebase Sign-In 的登入和登出狀態
enum SignInState {
case signedIn
case signedOut
}

// 定義 登入的方法
enum LoginMethod: String {
case email
case facebook
case google
}

// 管理身份驗證狀態
@Published var state: SignInState = .signedOut
// Email 帳號
@Published var email: String?
// 帳號照片
@Published var userPhoto: String?
// 保存登入的方法
@Published var loginMethod: LoginMethod?

實現 Firebase 登入以及註冊的功能

Firebase Authentication -> Sign-in method -> 電子郵件/密碼 ✅

在 function createUserloginUser 我都加入了 completion,這是為了在完成登入或是註冊後,我們可以繼續做後續的操作

    // Firebase 建立帳號
func createUser(email: String, password: String, completion: @escaping (String?) -> Void) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print(error.localizedDescription)
completion("\(error.localizedDescription)") // Pass nil to indicate failure
} else {
guard let user = result?.user else {
completion(nil)
return
}
print("User created successfully:", user.email!, user.uid)
completion("User created successfully") // Pass success message
}
}
}

// Firebase 登入帳號
func loginUser(email: String, password: String, completion: @escaping (String?) -> Void) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
// 登入失敗,回傳錯誤訊息
completion(error.localizedDescription)
} else {
// 登入成功,回傳成功訊息
completion("Login success")
print("Login success")
self.state = .signedIn
self.loginMethod = .email
}
}
}

在主頁面 Enrollview 中,就可以新增你剛剛設計的 Model 了

import SwiftUI
import FirebaseAuth


struct EnrollView: View {

@State var email: String = ""
@State var password: String = ""
@State var showBool: Bool = false

@State var alertTitle: String = ""
@State var loginShowAlert: Bool = false
@State var registerShowAlert: Bool = false


@StateObject var viewModel = AuthenticationViewModel()
...

接下來只需要在 Button 呼叫你的 viewModel 中的 createUserloginUser 就可以進行登入或是註冊的動作

                        Button {
// 使用 Firebase email 登入
viewModel.loginUser(email: email, password: password) { result in
if let result = result {
alertTitle = result
loginShowAlert = true
}
}
} label: {
Text("Login")
.font(.title3)
.foregroundColor(.white)
}
.frame(width: 100, height: 50)
.background(Color(red: 22/255, green: 72/255, blue: 99/255))
.cornerRadius(20)
.alert(alertTitle, isPresented: $loginShowAlert) {
Button("OK"){
if alertTitle == "Login success" {
// 發布通知
NotificationCenter.default.post(name: AllNotification.toViewController, object: nil)
}
}
}
                        Button {
// 使用 Firebase email 註冊
viewModel.createUser(email: email, password: password) { result in
if let result = result {
alertTitle = result
registerShowAlert = true
}
}
} label: {
Text("Register")
.font(.title3)
.foregroundColor(.white)
}
.frame(width: 100, height: 50)
.background(Color(red: 22/255, green: 72/255, blue: 99/255))
.cornerRadius(20)
.alert(alertTitle, isPresented: $registerShowAlert) {
Button("OK"){}
}

這裡的 completion closure 的優點在於,它允許你在登入後繼續執行後續的操作,像是顯示錯誤資訊在警示窗

登入成功後會進行跳轉,因為我這個專案只要是用 Storyboard 開發的,進行跳轉時,我使用了 Notification,其他的細節如下

實現 Google 登入的功能

Firebase Authentication -> Sign-in method -> Google ✅

打開完成設定後,Firebase 會請你下載最新的 GoogleService-Info.plist並拖入專案中,裡面包含了重要的 REVERSED_CLIENT_ID,找到後將 value 複製起來

接下來找到 URL Type 建立一個新的 URL,並在 URL Schemes,輸入REVERSE_CLIENT_ID

回到 AuthenticationViewModel 新增 Google 登入與登出的方法

    func googleSignIn() {
// 檢查是否有先前的 Sign-In
if GIDSignIn.sharedInstance.hasPreviousSignIn() {
GIDSignIn.sharedInstance.restorePreviousSignIn { [unowned self] user, error in
authenticateUser(for: user, with: error)
}
} else {
// 從 Firebase 應用程式取得clientID。clientID它從先前新增到專案中的GoogleService-Info.plist中取得。
guard let clientID = FirebaseApp.app()?.options.clientID else { return }

// 使用以下命令建立 Google 登入設定對象clientID.
let configuration = GIDConfiguration(clientID: clientID)

// 由於沒有使用視圖控制器 presentingViewController,來透過註解的共用實例擷取存取權限 UIApplication.,因此現在不建議直接使用UIWindow,而您應該使用場景。
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
guard let rootViewController = windowScene.windows.first?.rootViewController else { return }

// 然後,signIn()從類別的共用實例呼叫GIDSignIn以啟動登入程序。您傳遞配置物件和呈現控制器。
GIDSignIn.sharedInstance.signIn(with: configuration, presenting: rootViewController) { [unowned self] user, error in
authenticateUser(for: user, with: error)
}
}
}

private func authenticateUser(for user: GIDGoogleUser?, with error: Error?) {
// 處理錯誤並儘早從方法中返回它
if let error = error {
print(error.localizedDescription)
return
}
// 從實例中取得 idToken和。accessTokenuser
guard let authentication = user?.authentication, let idToken = authentication.idToken else { return }

let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: authentication.accessToken)

// 使用它們登入 Firebase。如果沒有錯誤,將狀態變更為 signedIn。
Auth.auth().signIn(with: credential) { [unowned self] (authResult, error) in
if let error = error {
print(error.localizedDescription)

} else {
state = .signedIn
loginMethod = .google
guard let email = authResult?.user.email else { return }
guard let userPhoto = authResult?.user.photoURL?.absoluteString else { return }
self.email = email
self.userPhoto = userPhoto
// 發布通知
NotificationCenter.default.post(name: AllNotification.toViewController, object: nil)
}
}
}

func googleSignOut() {
GIDSignIn.sharedInstance.signOut()
do {
try Auth.auth().signOut()
state = .signedOut
} catch {
print(error.localizedDescription)
}
}

回到主頁的 Enrollview,直接使用 viewModel 中的 googleSignIn 即可使用 Google 登入

                        Button {
viewModel.googleSignIn()
} label: {
Text("Gmail")
.font(.title3)
.foregroundColor(.black)
}
.frame(width: 100, height: 50)
.background(.white)
.cornerRadius(20)

上面實現的登入方法,不適用於最新的 googlesignin-ios 7.0.0,若是使用的話會出現一些錯誤,錯誤如下

GoogleSignIn: value of type GIDSignInResult has no member ‘authentication’

實現 Facebook 登入的功能

實現 Facebook 登入需要完成一些前置的作業,可以先實現 Facebook 登入的功能,可以參考下面的文章

Firebase Authentication -> Sign-in method -> Facebook ✅

接下來到 AuthenticationViewModel 新增 Facebook 登入和登出的功能

    // Facebook 登入帳號
func fbSignIn(completion: @escaping (String?) -> Void) {
let manager = LoginManager()
manager.logIn(permissions: [.publicProfile, .email]) { result in
switch result {
case .success(granted: _, declined: _, token: let token):
print("fb login success")
// 取得資訊
let request = GraphRequest(graphPath: "me", parameters: ["fields": "id, email, name"])
request.start { response, result, error in
if let result = result as? [String:String] {
print(result)
self.email = result["email"]
}
}
let credential = FacebookAuthProvider.credential(withAccessToken: token!.tokenString)
Auth.auth().signIn(with: credential) { result, error in
guard error == nil else {
print(error!.localizedDescription)
// 登入失敗,回傳錯誤訊息
completion(error!.localizedDescription)
return
}
// 登入成功,回傳成功訊息
completion("Login success")
print("login ok")
self.state = .signedIn
self.loginMethod = .facebook
}
case .cancelled:
print("cancelled")
case .failed(_):
print("failed")
}
}
}

// FaceBook 登出
func fbSignOut() {
do {
try Auth.auth().signOut()
let manager = LoginManager()
manager.logOut()
state = .signedOut
} catch {
print(error.localizedDescription)
}
}

回到主頁的 Enrollview,直接使用 viewModel 中的 fbSignIn 即可使用 Facebook 登入

                        Button {
viewModel.fbSignIn { result in
if let result = result {
alertTitle = result
loginShowAlert = true
}
}
} label: {
Text("FaceBook")
.font(.title3)
.foregroundColor(.white)
}
.frame(width: 100, height: 50)
.background(Color(red: 24/255, green: 119/255, blue: 242/255))
.cornerRadius(20)

這邊也需要注意 facebook-ios-sdk 的版本,最新版本的 facebook-ios-sdk 就不能使用上面方法進行登入,寫法有點不同

可能會出現的錯誤

若是已經完成 Facebook 登入後,再次啟動 APP 會出現以下的警示,目前找不太到方法解法

判斷是否已經登入

在 Enrollview 中,我使用了 .onAppear 來判斷是否有登入過,有的話就會進行跳轉,還有每一個裝置只會紀錄一種登入的方法

        .onAppear {
// 判斷是否有登入過
if let currentUser = Auth.auth().currentUser {
checkLoginInMethod(currentUser: currentUser)
// 發布通知
NotificationCenter.default.post(name: AllNotification.toViewController, object: nil)

}
}

抓取目前使用什麼方法進行登入

    // 判斷是用哪一種方法登入
func checkLoginInMethod(currentUser: User) {
// 使用 if 條件判斷
if currentUser.providerData.contains(where: { $0.providerID == "facebook.com" }) {
viewModel.loginMethod = .facebook
print("facebook.com")
} else if currentUser.providerData.contains(where: { $0.providerID == "google.com" }) {
viewModel.loginMethod = .google
print("google.com")
} else {
viewModel.loginMethod = .email
print("firebase")
}
}

需要注意的地方

在實現 Google 和 Facebook 登入時,都會需要在 URL Type 中的 URL Schemes 新增REVERSE_CLIENT_ID

⚠️ 這一個 REVERSE_CLIENT_ID 在你提交 Commit 時,它不應公開在公共儲存庫中以防止濫用。您需要在版本控制中忽略該文件以避免它被提交。

Reference

--

--