NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

FirebaseがSwiftのasync/awaitに対応したのでFirebase Authenticationで試してみた

Google I/O 2022でFirebase Apple SDK 9.0.0の発表、リリースがされました。
メジャーアップデートにより正式にSwiftへ対応したとの事で色々便利になっているらしいのでまずはよく使う認証機能にあたる「Firebase Authentication」を試してみました。

Google I/Oでの発表

概要:新しくなったFirebase

5/11から開催かれたGoogle I/O 2022でFirebaseについての発表、説明がありました。
詳細を知りたい方は以下動画と記事を参照ください。

io.google

firebase.blog

今回はこの中でもFirebaseがSwift対応したことによってasync/awaitで処理する事が可能になりました!
早速Firebase Authenticationを試してみました!

前提知識

この記事を読む対象読者としてFirebase Authenticationを知っていて尚且つSwiftでのasync/awaitについてザックリと理解している人を想定していますが、もしそうでなければ以下参考にしていただければと思います。

Firebase Authenticationとは

簡単に言うと認証機能をFirebase (クラウド)側でいい感じにしてくれるmBaaS(Mobile backend as a service)です。
詳しくは公式のコチラの記事をご覧ください。

Firebase Authentication  |  Firebase Documentation

Firebase Apple SDKの導入方法

今回のFirebaseのメジャーアップデートによりFirebase iOS SDKからFirebase Apple SDKと呼び方が変わっていますが公式の導入方法ではfirebase-ios-sdkと表示されています。
この記事ではFirebase Apple SDKで統一して表示させて頂きますのでご了承ください。

GitHubでの表示

Release Firebase Apple 9.0.0 · firebase/firebase-ios-sdk · GitHub

こちらリリースノート

firebase.google.com

またXcodeの最小必要バージョンは13.3.1となっていますのでご注意を。 導入方法については公式からとても丁寧に記載されているので以下参照ください。

firebase.google.com

余談ですがSwiftUI+SPMでの導入の場合、AppDelegateを自作しないといけなかったのですが以下のようにAppDelegateを書かなくても導入する事ができます。

import SwiftUI
import Firebase

@main
struct NewFirebaseAuthenticationDemoApp: App {
    init() {
            FirebaseApp.configure()
        }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

async/awaitについて

WWDC 2021で発表、リリースがされた非同期処理に関する機能です。
詳しくはコチラの動画と

developer.apple.com

以下の2記事で概要から使用方法まで網羅する事ができると思いますので是非詳しく知りたい方はご参照ください。

Swift Concurrency まとめ(正式版対応済)

Swift Concurrency チートシート

実践:Firebase Apple SDK 9.0.0でのFirebase Authenticationの使用方法

前置きが少し長くなってしまいましたが、実践について以下記載していきたいと思います。

環境

  • Xcode: 13.3.1
  • Firebase Apple SDK: 9.0.0

実践

まずはSign inから試してみたいと思います。 以前はコールバック関数での使用となっていましたのでこんな感じで使用していました。

func signin(email: String, password: String, failure:@escaping (String) -> Void, success:@escaping () -> Void) {
    Auth.auth().signIn(withEmail: email, password: password) { result, error in
        if error == nil {
            success()
        } else {
            if let errorCode = AuthErrorCode(rawValue: error!._code) {
                switch errorCode {
                case .invalidEmail:
                    failure(SigninError.InvalidEmail.title)
                case .weakPassword:
                    failure(SigninError.WrongPassword.title)
                case .wrongPassword:
                    failure(SigninError.WrongPassword.title)
                case .userNotFound:
                    failure(SigninError.UserDisabled.title)
                case .networkError:
                    failure(SigninError.NetworkError.title)
                default:
                    failure(SigninError.unknown.title)
                }
            }
        }
    }
}

withCheckedThrowingContinuationを使用する事でコールバックをラップしてasync / awaitに対応した書き方ができます。 こんな感じです。

func signin(email: String, password: String) async throws -> Void {
    return try await withCheckedThrowingContinuation{ continuation in
        Auth.auth().signIn(withEmail: email, password: password) { user, error in
            if error == nil {
                // サインインに成功した時の処理
                continuation.resume(returning: print("User ID:" + user!.user.uid))
            } else {
                // サインインに失敗した時の処理
                if let errorCode = AuthErrorCode(rawValue: error!._code) {
                    switch errorCode {
                    case .invalidEmail:
                        continuation.resume(throwing: SigninError.InvalidEmail)
                    case .weakPassword:
                        continuation.resume(throwing: SigninError.WrongPassword)
                    case .wrongPassword:
                        continuation.resume(throwing: SigninError.WrongPassword)
                    case .userNotFound:
                        continuation.resume(throwing: SigninError.UserDisabled)
                    case .networkError:
                        continuation.resume(throwing: SigninError.NetworkError)
                    default:
                        continuation.resume(throwing: SigninError.unknown)
                    }
                }
            }
        }
    }
}

では実際新しくなったAuthenticationを試してみます。

async/awaitに対応している事が確認できます。

func signInAndGetUid(email: String, password: String) async -> String {
    do {
        let result = try await Auth.auth().signIn(withEmail: email, password: password)
        let uid = result.user.uid
        return uid
    } catch {
         let errorCode = AuthErrorCode.Code(rawValue: error._code)
        switch errorCode {
        case .networkError:
            return AuthError.networkError.title
        case .weakPassword:
            return AuthError.weakPassword.title
        case .wrongPassword:
            return AuthError.wrongPassword.title
        case .userNotFound:
            return AuthError.userNotFound.title
        default:
            return AuthError.unknown.title
        }
    }
}

do-try-catchで書けるようになり、非常にみやすくなった印象です。 またthrowsを使用すればエラーハンドリングも別で記載する事ができるので更にシンプルに書く事ができます。

func signin(email: String, password: String) async throws -> String {
    let result = try await Auth.auth().signIn(withEmail: email, password: password)
    let uid = result.user.uid
    return uid
}

たったこれだけでサインインに成功すればuidを吐き出し、失敗すればthrowsを投げてくれます。

変わった点

上記のコード通り、まず非常にシンプルに書く事ができるようになりました。 またdo-try-catchで書けるようになり、非常に見やすくなったのもすごく良いです。 またthrowsを使用すれば更にシンプルに書く事ができ、functionの定義からたったの5行で実装する事ができます。

一通り試してみた

更にサインアップやサインアウト、パスワードの再設定とAuthenticationで必要になりそうな実装を一通り試してみました。 ここでは認証結果をStringで吐き出すサンプルとして記載しています。

  • enum エラーに対して何のエラーかStringを吐き出してくれるように定義します。
public enum AuthError: Error {
    // ネットワークエラー
    case networkError
    // パスワードが条件より脆弱であることを示します。
    case weakPassword
    // ユーザーが間違ったパスワードでログインしようとしたことを示します。
    case wrongPassword
    // ユーザーのアカウントが無効になっていることを示します。
    case userNotFound
    // メールアドレスの形式が正しくないことを示します。
    case invalidEmail
    // 既に登録されているメールアドレス
    case emailAlreadyInUse
    // 不明なエラー
    case unknown
    
    //エラーによって表示する文字を定義
    var title: String {
        switch self {
        case .networkError:
            return "通信エラーです。"
        case .weakPassword:
            return "パスワードが脆弱です。"
        case .wrongPassword:
            return "メールアドレス、もしくはパスワードが違います。"
        case .userNotFound:
            return "アカウントがありません。"
        case .invalidEmail:
            return "正しくないメールアドレスの形式です。"
        case .emailAlreadyInUse:
            return "既に登録されているメールアドレスです。"
        case .unknown:
            return "エラーが起きました。"
        }
    }
}
  • サインイン 上記にもありますが改めて。
func signInAndGetUid(email: String, password: String) async -> String {
    do {
        let result = try await Auth.auth().signIn(withEmail: email, password: password)
        let uid = result.user.uid
        return uid
    } catch {
        let errorCode = AuthErrorCode.Code(rawValue: error._code)
        switch errorCode {
        case .networkError:
            return AuthError.networkError.title
        case .weakPassword:
            return AuthError.weakPassword.title
        case .wrongPassword:
            return AuthError.wrongPassword.title
        case .userNotFound:
            return AuthError.userNotFound.title
        default:
            return AuthError.unknown.title
        }
    }
}
  • サインアップ
    こちらもasync/awaitへ対応している事が確認できます。
func signUpAndGetUid(email: String, password: String) async -> String {
    do {
        let result = try await Auth.auth().createUser(withEmail: email, password: password)
        let uid = result.user.uid
        return uid
    } catch {
         let errorCode = AuthErrorCode.Code(rawValue: error._code)
        switch errorCode {
        case .networkError:
            return AuthError.networkError.title
        case .weakPassword:
            return AuthError.weakPassword.title
        case .emailAlreadyInUse:
            return AuthError.emailAlreadyInUse.title
        default:
            return AuthError.unknown.title
        }
    }
}
  • サインアウト
    こちらは以前と同じくthrowを投げるだけの仕様です。
func signOut() async -> String {
    do {
        try Auth.auth().signOut()
        return "サインアウトしました"
    } catch {
        let errorCode = AuthErrorCode.Code(rawValue: error._code)
        switch errorCode {
        case .networkError:
            return AuthError.networkError.title
        default:
            return AuthError.unknown.title
        }
    }
}
  • パスワードの再設定
    こちらもasync/awaitへ対応している事が確認できます。
func passwordReset(email: String, password: String) async -> String {
    do {
        try await Auth.auth().sendPasswordReset(withEmail: email)
        return "パスワードを再設定するメールを送信しました。"
    } catch {
         let errorCode = AuthErrorCode.Code(rawValue: error._code)
         switch errorCode {
         case .networkError:
             return AuthError.networkError.title
         case .userNotFound:
             return AuthError.userNotFound.title
         case .invalidEmail:
             return AuthError.invalidEmail.title
         default:
             return AuthError.unknown.title
        }
    }
}

所感

サインアウトは以前からですが、その他もdo-try-catchで書けるようになったので非常に書きやすさと見やすくなった印象です。 またthrowsを使用する事でロジックに注力したり、認証してそのままFireStoreにuidを投げる事がシンプルに書けそうで非常に良いメジャーアップデートだと感じました。 次回は是非FireStoreも試してみたいと思いますので次回もお楽しみに!

執筆者: 岡優志(oka yuji)
元高校教員→モバイルエンジニアで主にiOSを担当。
Twitter: oka yuji