Zenn
🍏

TCAでFirebase Authenticationやってみた

2025/01/26に公開

結構難しい

この記事はTCAをカウンター以外で使ってみたい人むけの記事です。FirebaseとTCAやったことある人向けです。SwiftUIとxcodeもある程度知識がある人向けですね。

本作ってみたのでご興味ある人いたら読んでみてください。カウンターとログインのサンプルのリンクがあるぐらいですが(^^;;

https://zenn.dev/joo_hashi/books/fddc8771d2a9c3

これが作ったもの
https://youtu.be/b4dqM81ZtV4

追加するモジュール

こんな感じになってます。

こちらが完成品

サクッと作ります

結構難しかったです難しかったです(^_^;)
試作品なので参考にするぐらいでお願いいたします。

Userというモデルを作っておきます。

import Foundation

// ユーザーモデル
struct User: Equatable, Identifiable, Codable {
    let id: String     // ユーザーID
    let email: String  // メールアドレス
}

Firebase認証システム - TCA実装解説

状態管理(State)

  • ユーザー情報(user)
  • ローディング状態(isLoading)
  • エラー情報(error)
  • 入力フォーム(email, password)

アクション(Action)

  • フォーム更新: updateEmail, updatePassword
  • 認証操作: signIn, signUp, signOut
  • API応答: signInResponse, signUpResponse, signOutResponse

主な機能

  1. サインイン/サインアップ

    • 入力検証
    • ローディング制御
    • エラーハンドリング
  2. サインアウト

    • セッション管理
    • エラー処理
  3. 状態管理

    • ユーザーセッション
    • エラー状態
    • ローディング状態

このReducerは、Firebase認証のステート管理とビジネスロジックを分離し、副作用を制御可能な形で実装しています。

import ComposableArchitecture
import Foundation

struct AuthReducer: Reducer {
   struct State: Equatable {
       var user: User?
       var isLoading = false
       var error: AuthError?
       var email = ""
       var password = ""
   }
   
   enum AuthError: LocalizedError, Equatable {
       case signInError(String)
       case signUpError(String)
       case signOutError(String)
       
       var errorDescription: String? {
           switch self {
           case .signInError(let message),
                .signUpError(let message),
                .signOutError(let message):
               return message
           }
       }
   }
   
   enum Action: Equatable {
       case updateEmail(String)
       case updatePassword(String)
       case signIn
       case signUp
       case signOut
       case signInResponse(Result<User, AuthError>)
       case signUpResponse(Result<User, AuthError>)
       case signOutResponse(Bool)
   }
   
   @Dependency(\.authClient) var authClient
   
   func reduce(into state: inout State, action: Action) -> Effect<Action> {
       switch action {
       case let .updateEmail(email):
           state.email = email
           return .none
           
       case let .updatePassword(password):
           state.password = password
           return .none
           
       case .signIn:
           state.isLoading = true
           return .run { [email = state.email, password = state.password] send in
               do {
                   let user = try await authClient.signIn(email, password)
                   await send(.signInResponse(.success(user)))
               } catch {
                   await send(.signInResponse(.failure(.signInError(error.localizedDescription))))
               }
           }
           
       case .signUp:
           state.isLoading = true
           return .run { [email = state.email, password = state.password] send in
               do {
                   let user = try await authClient.signUp(email, password)
                   await send(.signUpResponse(.success(user)))
               } catch {
                   await send(.signUpResponse(.failure(.signUpError(error.localizedDescription))))
               }
           }
           
       case .signOut:
           state.isLoading = true
           return .run { send in
               do {
                   try await authClient.signOut()
                   await send(.signOutResponse(true))
               } catch {
                   await send(.signOutResponse(false))
               }
           }
           
       case let .signInResponse(.success(user)):
           state.user = user
           state.isLoading = false
           state.error = nil
           return .none
           
       case let .signInResponse(.failure(error)):
           state.error = .signInError(error.localizedDescription)
           state.isLoading = false
           return .none
           
       case let .signUpResponse(.success(user)):
           state.user = user
           state.isLoading = false
           state.error = nil
           return .none
           
       case let .signUpResponse(.failure(error)):
           state.error = .signUpError(error.localizedDescription)
           state.isLoading = false
           return .none
           
       case let .signOutResponse(success):
           state.isLoading = false
           if success {
               state.user = nil
               state.error = nil
           }
           return .none
       }
   }
}

Firebase認証クライアント - 実装解説

概要

認証操作をラップし、TCAで使用可能な形で提供するクライアント実装

構造

struct AuthClient {
    var signIn: (String, String) async throws -> User
    var signUp: (String, String) async throws -> User
    var signOut: () async throws -> Void
}

依存性注入

  • DependencyKeyプロトコルに準拠
  • liveValueでFirebase実装を提供
  • DependencyValuesで依存性を登録

主な機能

  1. サインイン: メール/パスワードで認証
  2. サインアップ: 新規ユーザー登録
  3. サインアウト: セッション終了

このクライアントは、Firebase認証をTCAの依存性注入システムに統合し、テスト可能で保守性の高い実装を提供します。

import Foundation
import Dependencies
import FirebaseAuth

// 認証クライアントのプロトコルを定義
struct AuthClient {
    // サインイン、サインアップ、サインアウトのメソッド
    var signIn: (String, String) async throws -> User
    var signUp: (String, String) async throws -> User
    var signOut: () async throws -> Void
}

// 依存性注入のための拡張
extension AuthClient: DependencyKey {
    // 実際の Firebase Authentication を使用した実装
    static var liveValue: AuthClient {
        return AuthClient(
            // サインイン処理
            signIn: { email, password in
                let result = try await Auth.auth().signIn(withEmail: email, password: password)
                return User(id: result.user.uid, email: result.user.email ?? "")
            },
            // サインアップ処理
            signUp: { email, password in
                let result = try await Auth.auth().createUser(withEmail: email, password: password)
                return User(id: result.user.uid, email: result.user.email ?? "")
            },
            // サインアウト処理
            signOut: {
                try Auth.auth().signOut()
            }
        )
    }
}

// 依存性の値に AuthClient を追加
extension DependencyValues {
    var authClient: AuthClient {
        get { self[AuthClient.self] }
        set { self[AuthClient.self] = newValue }
    }
}

認証画面(AuthView)の実装解説

コンポーネント構成

  1. メールアドレス入力(TextField)

    • キーボードタイプ: emailAddress
    • 自動大文字無効
  2. パスワード入力(SecureField)

    • セキュアな入力フィールド
  3. アクションボタン

    • サインイン/サインアップ
    • ローディング中は無効化
  4. 状態表示

    • ProgressView: ローディング表示
    • エラーメッセージ: 赤文字で表示

状態管理

  • WithViewStoreで状態監視
  • bindingを使用したフォーム入力
  • isLoadingによるUI制御

このビューは、TCAのストアと連携して認証フローのUIを提供します。

import SwiftUI
import ComposableArchitecture

// 認証画面のビュー
struct AuthView: View {
    let store: StoreOf<AuthReducer>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            VStack {
                // メールアドレス入力フィールド
                TextField("メールアドレス", text: viewStore.binding(
                    get: \.email,
                    send: AuthReducer.Action.updateEmail
                ))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .autocapitalization(.none)
                .keyboardType(.emailAddress)
                
                // パスワード入力フィールド
                SecureField("パスワード", text: viewStore.binding(
                    get: \.password,
                    send: AuthReducer.Action.updatePassword
                ))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                
                // サインインとサインアップボタン
                HStack {
                    Button("サインイン") {
                        viewStore.send(.signIn)
                    }
                    .disabled(viewStore.isLoading)
                    
                    Button("サインアップ") {
                        viewStore.send(.signUp)
                    }
                    .disabled(viewStore.isLoading)
                }
                
                // ローディングインジケーター
                if viewStore.isLoading {
                    ProgressView()
                }
                
                // エラーメッセージ
                if let error = viewStore.error {
                    Text(error.localizedDescription)
                        .foregroundColor(.red)
                }
            }
            .padding()
        }
    }
}

ログイン後の画面です。ボタンを押すとSignOutしてログインページへ戻ります。

import SwiftUI
import ComposableArchitecture

// ホーム画面のビュー
struct HomeView: View {
    let store: StoreOf<AuthReducer>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            VStack {
                // ユーザー歓迎メッセージ
                Text("ようこそ、\(viewStore.user?.email ?? "ユーザー")!")
                    .font(.title)
                    .padding()
                
                // サインアウトボタン
                Button("サインアウト") {
                    viewStore.send(.signOut)
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.large)
                
                // ローディングインジケーター
                if viewStore.isLoading {
                    ProgressView()
                }
            }
        }
    }
}

TCAを使用したFirebase認証アプリの主要構造

アプリケーション構成

@main
struct TcaFirebaseAuthenticationApp
  • Firebase初期化を管理
  • アプリのエントリーポイント

Firebaseセットアップ(AppDelegate)

  • 二重初期化防止
  • アプリ起動時の設定

メインビュー(ContentView)

  • 認証状態に基づくビュー切り替え
    • 認証済み: HomeView
    • 未認証: AuthView
  • TCAストアの初期化

状態管理

  • ObservableState準拠
  • ユーザー状態の監視
  • ビュー更新の制御

このコードは、TCAとFirebaseを統合し、認証フローを管理する基盤を提供します。

import SwiftUI
import ComposableArchitecture
import FirebaseCore

@main
struct TcaFirebaseAuthenticationApp: App {
   @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
   
   var body: some Scene {
       WindowGroup {
           ContentView()
       }
   }
}

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

struct ContentView: View {
   let store = Store(initialState: AuthReducer.State()) {
       AuthReducer()
   }
  
   var body: some View {
       WithViewStore(store, observe: { $0 }) { viewStore in
           Group {
               if viewStore.user != nil {
                   HomeView(store: store)
               } else {
                   AuthView(store: store)
               }
           }
       }
   }
}

extension AuthReducer.State: ObservableState {
   var _$id: ComposableArchitecture.ObservableStateID {
       .init()
   }
   
   mutating func _$willModify() {}
}

最後に

長い記事になりました。今回はTCAを使用して認証の状態管理をおこなうFirebase Authenticationと組み合わせたデモアプリを作成いたしました。データ型があってないとかエラー詰まったりしてキツかった😭

Discussion

ログインするとコメントできます