🎄

SwiftUIでFirebaseAuthを使ってみた

2023/11/13に公開2

Overview

SwiftUIでログイン機能を作ってみたいと思い今回は、FirebaseAuthを使ってみました。
https://firebase.google.com/docs/auth/ios/start?hl=ja

動画も作ってみました

https://www.youtube.com/watch?v=1STXnwaqMu0

今回実装する機能

  1. ログイン.
  2. 新規登録.
  3. パスワードのリセット.
  4. ログイン状態の維持.
認証状態をリッスンする

ログイン中のユーザーの情報を必要とするアプリの各ビューに対して、FIRAuth オブジェクトにリスナーをアタッチします。このリスナーは、ユーザーのログイン状態が変わるたびに呼び出されます。

ビュー コントローラの viewWillAppear メソッドでリスナーをアタッチします。

handle = Auth.auth().addStateDidChangeListener { auth, user in
  // ...
}
新しいユーザーを登録する

新規ユーザーがメールアドレスとパスワードを使用してアプリに登録できるフォームを作成します。ユーザーがフォームに入力したら、ユーザーから提供されたメールアドレスとパスワードを検証し、それらを createUser メソッドに渡します。

Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
  // ...
}
既存のユーザーをログインさせる
Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
  guard let strongSelf = self else { return }
  // ...
}
パスワードの再設定メールを送信する

sendPasswordReset メソッドを使用して、ユーザーにパスワードの再設定メールを送信できます。次に例を示します。

Auth.auth().sendPasswordReset(withEmail: email) { error in
  // ...
}

summary

SwiftUIの新規プロジェクトを作成して、FirebaseのSDKを追加する。
https://firebase.google.com/docs/ios/setup?hl=ja

これを追加して、FirebaseAuthのパッケージにチエックを入れる。

https://github.com/firebase/firebase-ios-sdk

SDKの設定をする

今回は、認証状態によって、画面遷移をする設定をするので、SDKの設定をするページは以下のように設定をする。

デフォルトの設定ファイルにFirebaseCoreを設定する
import SwiftUI
import FirebaseCore


class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    FirebaseApp.configure()

    return true
  }
}


@main
struct SwiftUiFirebaseApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
        @StateObject var viewModel = AuthViewModel()

        var body: some Scene {
            WindowGroup {
                // ログイン状態によって画面遷移するページを変更する
                if viewModel.isAuthenticated {
                    HelloPage(viewModel: viewModel)
                } else {
                    SignInView(viewModel: viewModel)
                }
            }
        }
}

認証用のクラスを設計する

ViewModelって名前も変ですけど、こちらに認証に必要なロジックをまとめました。

認証のロジックをまとめたクラス
import SwiftUI
import FirebaseAuth

class AuthViewModel: ObservableObject {
    @Published var isAuthenticated = false
    // イニシャライザメソッドを呼び出して、アプリの起動時に認証状態をチェックする
    init() {
            observeAuthChanges()
        }

        private func observeAuthChanges() {
            Auth.auth().addStateDidChangeListener { [weak self] _, user in
                DispatchQueue.main.async {
                    self?.isAuthenticated = user != nil
                }
            }
        }
    // ログインするメソッド
    func signIn(email: String, password: String) {
            Auth.auth().signIn(withEmail: email, password: password) { [weak self] result, error in
                DispatchQueue.main.async {
                    if result != nil, error == nil {
                        self?.isAuthenticated = true
                    }
                }
            }
        }
    // 新規登録するメソッド
    func signUp(email: String, password: String) {
        Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
            DispatchQueue.main.async {
                if result != nil, error == nil {
                    self?.isAuthenticated = true
                }
            }
        }
    }
    // パスワードをリセットするメソッド
    func resetPassword(email: String) {
        Auth.auth().sendPasswordReset(withEmail: email) { error in
            if let error = error {
                print("Error in sending password reset: \(error)")
            }
        }
    }
    // ログアウトするメソッド
    func signOut() {
            do {
                try Auth.auth().signOut()
                isAuthenticated = false
            } catch let signOutError as NSError {
                print("Error signing out: %@", signOutError)
            }
        }
}

認証のページとログイン後のページを作る

アプリの起動時は、ログインしていなかったら、SignInViewを表示する。

ログインページ
import SwiftUI

struct SignInView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    @ObservedObject var viewModel: AuthViewModel

    var body: some View {
        NavigationView {
            VStack {
                TextField("Email", text: $email)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()

                SecureField("Password", text: $password)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()

                Button("Sign In") {
                    viewModel.signIn(email: email, password: password)
                }

                if viewModel.isAuthenticated {
                    // ログイン後のページに遷移
                    HelloPage(viewModel: viewModel)
                }

                // 新規登録画面への遷移ボタン
                NavigationLink(destination: SignUpView(viewModel: viewModel)) {
                    Text("Create Account")
                        .padding(.top, 16)
                }
                // パスワードのリセットページへ移動する
                NavigationLink(destination: ResetPasswordView(viewModel: viewModel)) {
                    Text("Password Reset")
                        .padding(.top, 16)
                }
            }
        }
    }
}

新規登録のページ。こちらが最初に使うことになると思うページ。ここでユーザーの新規登録をおこなう。

新規登録ページ
import SwiftUI

struct SignUpView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    @ObservedObject var viewModel: AuthViewModel

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            SecureField("Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            Button("Sign Up") {
                viewModel.signUp(email: email, password: password)
            }

            if viewModel.isAuthenticated {
                // ログイン後のページに遷移
                if viewModel.isAuthenticated {
                    HelloPage(viewModel: viewModel)
                }

            }
        }
    }
}

もし、パスワードを忘れたら、こちらのページでリセットできます。実在するパスワードにしか招待メールが届かないので注意!

パスワードを忘れたときのページ
import SwiftUI

struct ResetPasswordView: View {
    @State private var email: String = ""
    @ObservedObject var viewModel: AuthViewModel

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            Button("Reset Password") {
                viewModel.resetPassword(email: email)
            }
        }
    }
}

ログイン後のページ。こちらは認証が維持されていれば、自動的に表示されます。ログアウトのボタンもつけておきました。

Hello Worldを表示する
import SwiftUI

// ログイン後の画面
struct HelloPage: View {
    var viewModel: AuthViewModel

    var body: some View {
        VStack {
            Text("Hello, you're logged in!")
                .font(.title)
                .padding()
            Button("Log Out") {
                // ログアウトしてログイン画面へ遷移する
                viewModel.signOut()
            }
        }
    }
}

ビルドするとこんな感じです



パスワードのリセット
パスワードをリセットに成功すると、招待メールが届きます。



thoughts

今回は、SwiftUIでFirebaseAuthのメールアドレス・パスワード認証を実装してみました。ただログインするだけではなくて、ログイン状態を維持したり、パスワードのリセット機能もつけたので、個人開発で十分使えるレベルではないかと思われます。
ぜひ、参考にしてみてください。

Discussion

ムウラムウラ

JBoyさん初めまして、ムウラと申します。
素晴らしい検証結果、ありがとうございます!
こちらでも無事、同様のログインを実装できました。
これほどシンプルな構成で、Firebase Authを導入できることに感動しております!

そしてViewModelの作り方が洗練されてて、とても勉強になりました。
一個一個のViewにViewModelが付くものだと思ってましたが、この構造は非常に合理的ですね!

今後ともよろしくお願いいたします(*_ _)ペコリ

JboyHashimotoJboyHashimoto

ムウラさんコメントありがとうございます!
いや〜これは、ほんとはViewModelとは呼ばないと思います〜Repostioryクラス作ってそこにロジック書いて、ViewModelのクラスは、エラー返すとか状態を通知する使い方が正しいと思います。
あってるかわからないですが。