SwiftUIでFirebaseAuthを使ってみた
Overview
SwiftUIでログイン機能を作ってみたいと思い今回は、FirebaseAuthを使ってみました。
動画も作ってみました
今回実装する機能
- ログイン.
- 新規登録.
- パスワードのリセット.
- ログイン状態の維持.
認証状態をリッスンする
ログイン中のユーザーの情報を必要とするアプリの各ビューに対して、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を追加する。
これを追加して、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が付くものだと思ってましたが、この構造は非常に合理的ですね!
今後ともよろしくお願いいたします(*_ _)ペコリ
ムウラさんコメントありがとうございます!
いや〜これは、ほんとはViewModelとは呼ばないと思います〜Repostioryクラス作ってそこにロジック書いて、ViewModelのクラスは、エラー返すとか状態を通知する使い方が正しいと思います。
あってるかわからないですが。