🪶

SwiftUI Email Password Sign In

2024/07/15に公開

記事の対象者

  • SwiftUIを学習している人
  • Firebaseを使った経験がある人
  • FirebaseAuthenticationを学んでみたい人

🔓FirebaseAuthを使ってみる

FirebaseAuthenticationとは、Googleの提供しているFirebaseで使うことができる認証機能のサービスです。
SwiftUIで、メールアドレス&パスワード認証を使ってみる記事を書いてみました。以前他の記事でも書いたことありますが、今回は、もっと簡単なデモアプリにして作ってみます。

🔑実装する機能

基本的な機能だけつけます。

  1. 新規登録
  2. ログイン
  3. ログアウト
  4. ログインの維持

プロジェクトを作成

Firebaseでプロジェクトを作成してメールアドレス・パスワード認証を使えるようにしておいてください。



有効にして保存すればOK

SwiftUIの新規プロジェクトを作成する

SwiftUIで練習用のプロジェクトを作成する。

公式を参考に、SPMで firebase-ios-sdk を追加する。

https://firebase.google.com/docs/auth/ios/start?hl=ja

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

Firebaseのコンソールで、バンドルIDを設定する。

GoogleService-info.plistをダウンロードして、SwiftUIに追加する。


認証機能に必要なロジックを作る

メールアドレス・パスワードで、新規追加、ログイン、ログアウト、ログインの維持する機能を作っていこうと思います。

こちらを参考に作っていく
https://firebase.google.com/docs/auth/ios/manage-users?hl=ja

現在ログインしているユーザーを取得する
現在ログインしているユーザーを取得するには、Auth オブジェクトでリスナーを設定することをおすすめします。

handle = Auth.auth().addStateDidChangeListener { auth, user in
  // ...
}

リスナーを使うと、現在ログインしているユーザーを取得するときに Auth オブジェクトが中間状態(初期化など)ではないことを確認できます。

currentUser を使用することでも、現在ログインしているユーザーを取得できます。ユーザーがログインしていない場合、currentUser は nil です。

if Auth.auth().currentUser != nil {
  // User is signed in.
  // ...
} else {
  // No user is signed in.
  // ...
}

ViewModelを作成する。ロジックを中に書いてるので、ViewModelとは言えないかもしれません。画面の状態を持っているクラスだと思ってください。

import FirebaseAuthの箇所でエラーが出ていたら、このように対策してください。



数秒経つと赤いエラーが消えるはずです。

observeAuthChangesがログインを維持するメソッド、後はお馴染みのsignUp, signIn, signOutです。

ViewModel
import SwiftUI
import FirebaseAuth

class AuthViewModel: ObservableObject {
    @Published var isAuthenticated = false
    @Published var errorMessage: String?
    
    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 let error = error {
                    self?.errorMessage = error.localizedDescription
                } else {
                    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 let error = error {
                    self?.errorMessage = error.localizedDescription
                } else {
                    self?.isAuthenticated = true
                }
            }
        }
    }
    
    func signOut() {
        do {
            try Auth.auth().signOut()
            isAuthenticated = false
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

今回は、より簡単なサンプルにするために、新規登録とログイン画面を同じページにしました。認証がされると画面遷移をします。失敗したらエラーが出ます。

ログインページ
import SwiftUI

struct ContentView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    @StateObject var vm = AuthViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextField("Email", text: $email)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                SecureField("Password", text: $password)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                Button("Sign Up") {
                    vm.signUp(email: email, password: password)
                }
                
                Button("Sign In") {
                    vm.signIn(email: email, password: password)
                }
                
                if let errorMessage = vm.errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                }
                
                NavigationLink(destination: HelloPage(vm: vm), isActive: $vm.isAuthenticated) {
                    EmptyView()
                }
            }
        }
    }
}

こちらがログイン後のページです。ログアウトするボタンを押すと、ログアウトします。

Hello Page
import SwiftUI

struct HelloPage: View {
    @ObservedObject var vm: AuthViewModel
    
    var body: some View {
        VStack {
            Text("Welcome to Hello Page!")
            Button("Sign Out") {
                vm.signOut()
            }
        }
    }
}

ユーザーのログイン状態で、画面を切り替えるロジック設定すれば完成です。

アプリのエントリーポイント
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 EmailSignInExampleApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject var vm = AuthViewModel()
    
    var body: some Scene {
        WindowGroup {
            // ログイン状態によって画面遷移するページを変更する
            if vm.isAuthenticated {
                HelloPage(vm: vm)
            } else {
                ContentView(vm: vm)
            }
        }
    }
}

実際の動作


一度アプリを停止してまたビルドしてもこちらの画面が表示されていればログインが維持されています。

まとめ

過去に記事を書いたことがあったのですが、画面遷移のコードが非推奨のNavigationViewのままだったので、NavigationStackに修正して新しく作りました。こちらを参考に、SwiftUIでログインの練習をしてみてください。

補足情報

ログインをしたときに、スタックが重なって、バックボタンが表示されるので、こちらのコードに修正して、使った方が良いです。

import SwiftUI

struct ContentView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    
    
    @StateObject var vm = AuthViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextField("Email", text: $email)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                SecureField("Password", text: $password)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                Button("Sign Up") {
                    vm.signUp(email: email, password: password)
                }
                
                Button("Sign In") {
                    vm.signIn(email: email, password: password)
                }
                
                if let errorMessage = vm.errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                }
            }
            .navigationBarHidden(true)
                        .fullScreenCover(isPresented: $vm.isAuthenticated) {
                            HelloPage(vm: vm)
                        }
        }
    }
}

Discussion