🔑

SwiftUI × Firebase Authで認証機能を追加する ~実装編~(2/2)

に公開

1. はじめに

本記事は、以下の記事の続きになります。

https://zenn.dev/jnch/articles/d02c40600b3cd8

2. 認証機能を実装

今回は以下の構成で作成しようと思います。

認証用クラス(AuthViewModel)を作成する

AuthViewModel.swift
import SwiftUI
import FirebaseAuth
import GoogleSignIn
import FirebaseCore

class AuthViewModel: ObservableObject {
    @Published var user: User?
    @Published var isAuthenticated: Bool = false
    @Published var errorMessage: String = ""
    
    init() {
        checkUserState()
    }
    
    func checkUserState() {
        if let user = Auth.auth().currentUser {
            self.user = user
            self.isAuthenticated = true
        } else {
            self.isAuthenticated = false
            self.user = nil
        }
    }
    
    // メールとパスワードでログイン
    func signInWithEmail(email: String, password: String) {
        Auth.auth().signIn(withEmail: email, password: password) { [weak self] result, error in
            guard let self = self else { return }
            
            if let error = error {
                self.errorMessage = error.localizedDescription
                return
            }
            
            if let user = result?.user {
                self.user = user
                self.isAuthenticated = true
            }
        }
    }
    
    // メールとパスワードで新規登録
    func signUpWithEmail(email: String, password: String) {
        Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
            guard let self = self else { return }
            
            if let error = error {
                self.errorMessage = error.localizedDescription
                return
            }
            
            if let user = result?.user {
                self.user = user
                self.isAuthenticated = true
            }
        }
    }
    
    // Googleアカウントでログイン
    func signInWithGoogle() {
        guard let clientID = FirebaseApp.app()?.options.clientID else { return }
        
        let config = GIDConfiguration(clientID: clientID)
        
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let rootViewController = windowScene.windows.first?.rootViewController else {
            return
        }
        
        GIDSignIn.sharedInstance.signIn(
            withPresenting: rootViewController,
            hint: nil,
            additionalScopes: []
        ) { [weak self] result, error in
            guard let self = self else { return }
            
            if let error = error {
                self.errorMessage = error.localizedDescription
                return
            }
            
            guard let user = result?.user,
                  let idToken = user.idToken?.tokenString else {
                return
            }
            
            let credential = GoogleAuthProvider.credential(
                withIDToken: idToken,
                accessToken: user.accessToken.tokenString
            )
            
            // Firebaseにログイン
            Auth.auth().signIn(with: credential) { result, error in
                if let error = error {
                    self.errorMessage = error.localizedDescription
                    return
                }
                
                if let user = result?.user {
                    self.user = user
                    self.isAuthenticated = true
                }
            }
        }
    }
    
    // ログアウト
    func signOut() {
        do {
            try Auth.auth().signOut()
            self.isAuthenticated = false
            self.user = nil
        } catch let error {
            self.errorMessage = error.localizedDescription
        }
    }
}

認証画面(LoginView/EmailAuthView)を作成する

LoginView.swift
import SwiftUI

struct LoginView: View {
    @EnvironmentObject var authViewModel: AuthViewModel
    @State private var showingEmailAuth = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("認証アプリ")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.top, 50)
            
            Spacer()
            
            VStack(spacing: 15) {
                Button(action: {
                    showingEmailAuth = true
                }) {
                    HStack {
                        Image(systemName: "envelope.fill")
                            .foregroundColor(.white)
                        Text("メールで認証")
                            .foregroundColor(.white)
                            .fontWeight(.semibold)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
                }
                
                Button(action: {
                    authViewModel.signInWithGoogle()
                }) {
                    HStack {
                        Image(systemName: "g.circle.fill")
                            .foregroundColor(.white)
                        Text("Googleで認証")
                            .foregroundColor(.white)
                            .fontWeight(.semibold)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(10)
                }
            }
            .padding(.horizontal, 30)
            
            Spacer()
            
            if !authViewModel.errorMessage.isEmpty {
                Text(authViewModel.errorMessage)
                    .foregroundColor(.red)
                    .padding()
            }
        }
        .sheet(isPresented: $showingEmailAuth) {
            EmailAuthView()
                .environmentObject(authViewModel)
        }
    }
}
EmailAuthView.swift
import SwiftUI

struct EmailAuthView: View {
    @EnvironmentObject var authViewModel: AuthViewModel
    @Environment(\.presentationMode) var presentationMode
    
    @State private var email = ""
    @State private var password = ""
    @State private var isSignUp = false
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text(isSignUp ? "アカウント作成" : "ログイン")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding(.top, 30)
                
                VStack(spacing: 15) {
                    TextField("メールアドレス", text: $email)
                        .autocapitalization(.none)
                        .keyboardType(.emailAddress)
                        .padding()
                        .background(Color.gray.opacity(0.1))
                        .cornerRadius(10)
                    
                    SecureField("パスワード", text: $password)
                        .padding()
                        .background(Color.gray.opacity(0.1))
                        .cornerRadius(10)
                }
                .padding(.horizontal, 30)
                .padding(.top, 20)
                
                Button(action: {
                    if isSignUp {
                        authViewModel.signUpWithEmail(email: email, password: password)
                    } else {
                        authViewModel.signInWithEmail(email: email, password: password)
                    }
                    
                    if authViewModel.isAuthenticated {
                        presentationMode.wrappedValue.dismiss()
                    }
                }) {
                    Text(isSignUp ? "登録する" : "ログインする")
                        .foregroundColor(.white)
                        .fontWeight(.semibold)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                        .padding(.horizontal, 30)
                }
                
                Button(action: {
                    isSignUp.toggle()
                }) {
                    Text(isSignUp ? "既にアカウントをお持ちの方はこちら" : "アカウントをお持ちでない方はこちら")
                        .foregroundColor(.blue)
                }
                
                Spacer()
                
                if !authViewModel.errorMessage.isEmpty {
                    Text(authViewModel.errorMessage)
                        .foregroundColor(.red)
                        .padding()
                }
            }
            .navigationBarItems(leading: Button("キャンセル") {
                presentationMode.wrappedValue.dismiss()
            })
        }
    }
}
ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject private var authViewModel = AuthViewModel()
    
    var body: some View {
        Group {
            if authViewModel.isAuthenticated {
                MainView()
                    .environmentObject(authViewModel)
            } else {
                LoginView()
                    .environmentObject(authViewModel)
            }
        }
        .onAppear {
            authViewModel.checkUserState()
        }
    }
}

エントリーポイント(YourApp)を作成する

AuthApp.swift
import SwiftUI
import Firebase
import GoogleSignIn

@main
struct AuthApp: 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 {
        FirebaseApp.configure()
        return true
    }
    
    // Google認証のためのURLハンドリング
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        return GIDSignIn.sharedInstance.handle(url)
    }
}

メイン画面(MainView)を作成する

MainView.swift
import SwiftUI

struct MainView: View {
    @EnvironmentObject var authViewModel: AuthViewModel
    
    var body: some View {
        VStack {
            Text("Hello World")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding()
                
            Button(action: {
                authViewModel.signOut()
            }) {
                Text("ログアウト")
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(10)
            }
            .padding()
        }
    }
}

動作確認

画面は以下のようになります。
また、一度ログインすればアプリを再起動しても直接メイン画面に遷移します。

アプリ画面

Firebase管理画面上から見ると…
動作確認結果

3. まとめ

今回はSwiftUIFirebase Authを使ってiOSアプリに認証機能を追加しました。
便利なライブラリが用意されているため、容易に実装が可能であることがわかっていただけたかと思います。本記事により、皆さんのアプリ開発の幅が広がることを願います。

関連記事

https://zenn.dev/jnch/articles/28c6a780c57fdf

Discussion