🍏
TCAでFirebase Authenticationやってみた
結構難しい
この記事はTCAをカウンター以外で使ってみたい人むけの記事です。FirebaseとTCAやったことある人向けです。SwiftUIとxcodeもある程度知識がある人向けですね。
本作ってみたのでご興味ある人いたら読んでみてください。カウンターとログインのサンプルのリンクがあるぐらいですが(^^;;
これが作ったもの
追加するモジュール
こんな感じになってます。
サクッと作ります
結構難しかったです難しかったです(^_^;)
試作品なので参考にするぐらいでお願いいたします。
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
主な機能
-
サインイン/サインアップ
- 入力検証
- ローディング制御
- エラーハンドリング
-
サインアウト
- セッション管理
- エラー処理
-
状態管理
- ユーザーセッション
- エラー状態
- ローディング状態
この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
で依存性を登録
主な機能
- サインイン: メール/パスワードで認証
- サインアップ: 新規ユーザー登録
- サインアウト: セッション終了
このクライアントは、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)の実装解説
コンポーネント構成
-
メールアドレス入力(TextField)
- キーボードタイプ: emailAddress
- 自動大文字無効
-
パスワード入力(SecureField)
- セキュアな入力フィールド
-
アクションボタン
- サインイン/サインアップ
- ローディング中は無効化
-
状態表示
- 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