~~Firebase_auth_for_IOS~~

Back to Top

links




アプリ概要

今回は、ユーザの登録・認証を行うアプリを作成しました。
今回もFirebaseによりこれらの機能を実現しました。
まずはトップ画面です。

この画面では、ユーザ登録されている利用者に登録時のメールアドレスとパスワードを入力してもらいます。 入力が完了し、認証成功するとマイページへ遷移します。
また、まだ登録していないユーザは「登録されていない方はこちら」ボタンを押下し、Sign up画面に遷移して 登録してもらいます。

この画面はSignUp画面です。
ユーザ登録していない場合はメールアドレスとパスワードを入力して登録を行います。 登録済みの方が再度同じメールアドレスで登録をしようとすると、登録に失敗し二重登録 ができないようになっています。

次にマイページ画面です。
マイページ画面にはユーザ情報をコンソールに表示するボタンとログアウトのボタンが設置されています。 ログアウトボタンを押下すると自動的にトップページに遷移します。

コード(全体)

以下がコードの全体像になります。

ContentView.swift


import SwiftUI
import FirebaseAuth
import FirebaseFirestore

struct ContentView: View {
    
    let db = Firestore.firestore()
    var userInfo:UserInfo = UserInfo()
    var userAuth:UserAuth = UserAuth()
    
    @State var mail:String = ""
    @State var pass:String = ""
    @State var errorMessage:String = ""
    @State var isLogin:Bool = false
    @State var uid:String = ""
    
    var body: some View {
        NavigationView{
            VStack {
                TextField("mailaddress", text: $mail)
                TextField("password", text: $pass)
                Button(action:{
                    // 入力欄のチェック 正しい入力: "", 入力エラー: エラーメッセージ
                    errorMessage = userInfo.checkEntry(email: mail, pass: pass)
                    
                    if errorMessage == ""{
                        // ユーザ認証
                        Auth.auth().signIn(withEmail: mail, password: pass) {(authResult, error) in
                            guard authResult?.user != nil else{
                                print("認証失敗")
                                return
                            }
                            if let user = authResult?.user{
                                uid = user.uid
                                mail = user.email ?? ""
                                
                                // 新規ユーザのドキュメントの作成。 既存ユーザの場合は作成されない。
                                userAuth.accessUserDB(uid: uid, email: mail)
                                // 画面遷移するために変換
                                isLogin = true
                            }
                        }
                    }else{
                        print(errorMessage)
                    }
                }){
                    Text("ログイン")
                }.padding()
                
                // sign in画面への遷移
                NavigationLink(destination: SignUpView()){
                    Text("登録されていない方はこちら")
                }
                
                NavigationLink(destination: UserView(uid: uid), isActive: $isLogin){
                    EmptyView()
                }
            }
            .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}    

SignUpView.swift


import SwiftUI
import FirebaseAuth
import FirebaseFirestore

struct SignUpView: View {
    
    let userAuth:UserAuth = UserAuth()
    let userInfo:UserInfo = UserInfo()
    
    @State var mail:String = ""
    @State var pass:String = ""
    @State var errorMessage = ""
    var body: some View {
        VStack{
            TextField("mailaddress", text: $mail)
            TextField("password", text: $pass)
            Button(action:{
                
                print("登録ボタン押下")
                // 入力欄のチェック 入力不備あり: エラーメッセージ, 不備なし: ""
                errorMessage = userInfo.checkEntry(email: mail, pass: pass)
                
                if errorMessage == ""{
                    // 新規ユーザの作成
                    userAuth.createUser(email: mail, pass: pass)
                }else{
                    print(errorMessage)
                }
            }){
                Text("Sign Up")
            }.padding()
        }
    }
}    

UserView.swift


import SwiftUI
import FirebaseFirestore
import FirebaseAuth

struct UserView: View {
        
    var uid:String
    var db = Firestore.firestore()
    var userInfo:UserInfo = UserInfo()
    let userAuth:UserAuth = UserAuth()
    @State var isLogout = false     // ログイン画面へ戻るための変数
    
    init(uid: String) {
        self.uid = uid
    }
    
    var body: some View {
        NavigationView{
            VStack{
                Text("Hello")
                Button(action:{
                    // 登録されているユーザの情報を取得して表示
                    userInfo.getUserInfo(uid: uid)
                }){
                    Text("ユーザ情報の表示")
                }.padding()
                
                Button(action:{
                    // ログアウト処理
                    isLogout = userAuth.logOut()
                }){
                    Text("ログアウト")
                }
                
            }.navigationTitle("マイページ")
        }
        
        NavigationLink(destination: ContentView(), isActive: $isLogout){
            EmptyView()
        }
        
    }
}

UserInfo.swift


import Foundation
import FirebaseFirestore

class UserInfo{
    
    let db = Firestore.firestore()
    
    // Firestoreに登録されているユーザ情報の表示
    func getUserInfo(uid:String){
        self.db.collection("users").document(uid).getDocument{(document, error) in
            
            if let document = document{
                let dataDescription = document.data().map(String.init(describing: )) ?? "nil"
                print("Document data: \(dataDescription)")
            }
        }
    }
    
    // 入力欄の判定
    func checkEntry(email:String, pass:String) -> String{
        if email == ""{
            return "メールアドレスを入力してください。"
        }else if pass == ""{
            return "パスワードを入力してください"
        }else{
            return ""
        }
    }
    
    // 新規ログインユーザのドキュメント作成
    func createNewUserDocument(uid:String, email:String){
        self.db.collection("users").document(uid).setData([
            "uid" : uid,
            "email" : email,
            "display_name" : "anonymous",
            "create_date" : FirebaseFirestore.Timestamp()
        ]){error in
            if let error = error{
                print("Oh no!! this program has error!:\(error)")
            }else{
                print("success!")
            }
        }
    }
}

UserAuth.swift


import Foundation
import FirebaseAuth
import FirebaseFirestore

class UserAuth{
    
    var uid:String = ""
    var email:String = ""

    let db = Firestore.firestore()
    let userInfo = UserInfo()
    
    // ユーザの新規作成
    func createUser(email: String, pass: String){
        Auth.auth().createUser(withEmail: email, password: pass) { authResult, error in
            guard authResult?.user != nil else{
                print("登録できません")
                return
            }
        }
    }
    // Firestoreへのアクセス
    func accessUserDB(uid:String, email:String){
        self.db.collection("users").document(uid).getDocument(){(document, error) in
            if let document = document {
                if document.exists{
                    print("already done")
                }else{
                    // 新規ユーザの場合はドキュメントの作成
                    self.userInfo.createNewUserDocument(uid: uid, email:email)
                }
            }
        }
    }
    
    // ログアウト処理
    func logOut() -> Bool{
        let firebaseAuth = Auth.auth()
        var isSuccess = false
        do {
            try firebaseAuth.signOut()
            defer{
                isSuccess = true
            }
        } catch let signOutError as NSError {
            print("Error signing out: %@", signOutError)
        }
        return isSuccess
    }
    
    // userIdの取得
    func getUserId() -> String{
        return self.uid
    }
    
    // emailの取得
    func getUserEmail() -> String{
        return self.email
    }
}

コード解説

今回はFirebaseによる認証について解説していきます。

新規ユーザの作成


// UserAuth.swift
Auth.auth().createUser(withEmail: email, password: pass) { authResult, error in
    guard authResult?.user != nil else{
        print("登録できません")
        return
    }
}

このプログラムにより、ユーザの新規作成が行われます。 また、既存のユーザが登録しようとした場合はエラーとなり、登録に失敗します。

ログイン処理


// ContentView.swift
Auth.auth().signIn(withEmail: mail, password: pass) {(authResult, error) in
    guard authResult?.user != nil else{
        print("認証失敗")
        return
    }
    if let user = authResult?.user{
        uid = user.uid
        mail = user.email ?? ""
        
        // 新規ユーザのドキュメントの作成。 既存ユーザの場合は作成されない。
        userAuth.accessUserDB(uid: uid, email: mail)
        // 画面遷移するために変換
        isLogin = true
    }
}

ログインはこの処理で実装できます。
本来は「UserAuth」クラスに記述したかったのですが、 呼び出してもうまく動作できなかったので、今回は「ContentView」の中に 記述しました。
クロージャの引数の「authResult」にはログイン情報が、「error」には エラー情報が格納されています。クロージャ内ではまず、guard文によって authResultがnilでないことを保証しています。
authResultにはログインが成功しなかった場合はauthResultにnilが格納され、 成功した場合はログインしたユーザのログイン情報が格納されます。
guard文を抜けた後は、ログイン情報から、ユーザIDとメールアドレスを取得しています。その後は、 ユーザが初ログインの場合はユーザ情報を格納したドキュメントを作成し、マイページ画面へと遷移しています。

ログアウト処理


// UserAuth.swift
do {
    try firebaseAuth.signOut()
    defer{
        isSuccess = true
    }
} catch let signOutError as NSError {
    print("Error signing out: %@", signOutError)
}

ログアウトはこのコードで行なっています。
try・catch文によってログアウト失敗時のエラー処理を行なっています。 また、try文の後ろにあるdefer文はログアウトが成功した時のみ実行されます。 isSuccess変数は、画面を遷移させる戻り値のための変数になります。

ドキュメントの存在の確認


// UserAuth.swift
    func accessUserDB(uid:String, email:String){
    self.db.collection("users").document(uid).getDocument(){(document, error) in
        if let document = document {
            if document.exists{
                print("already done")
            }else{
                // 新規ユーザの場合はドキュメントの作成
                self.userInfo.createNewUserDocument(uid: uid, email:email)
            }
        }
    }
}

ユーザごとのドキュメントは、そのユーザの初回ログイン時に作成されます。 そのため、まずログインが成功したら、そのユーザ専用のドキュメントが存在するかを調べます。 この関数では、存在した場合コンソールに「already done」と表示し、存在しない場合は、 ドキュメント作成を行う関数を呼び出します。

ドキュメントの作成


// UserInfo.swift
    func createNewUserDocument(uid:String, email:String){
    self.db.collection("users").document(uid).setData([
        "uid" : uid,
        "email" : email,
        "display_name" : "anonymous",
        "create_date" : FirebaseFirestore.Timestamp()
    ]){error in
        if let error = error{
            print("Oh no!! this program has error!:\(error)")
        }else{
            print("success!")
        }
    }
}

この関数では、新たにユーザごとのドキュメントを作成しています。
データの登録時にエラーが発生したら、コンソールにエラー情報を表示し、 問題がなければコンソールに「success」と表示されます。

データの取得


func getUserInfo(uid:String){
    self.db.collection("users").document(uid).getDocument{(document, error) in
        
        if let document = document{
            let dataDescription = document.data().map(String.init(describing: )) ?? "nil"
            print("Document data: \(dataDescription)")
        }
    }
}

この関数によってユーザごとの情報を取得しています。
ユーザを指定するために、引数のuidによってドキュメントのパスを指定しています。

Firestoreのセキュリティルール


rules_version = '2';
service cloud.firestore {
    match /databases/{database}/documents {
        match /users/{userId}{
            allow write: if 
                request.auth != null;
            
            allow read, delete: if
                request.auth.uid == userId;
        }
    }
}

今回、認証の実装を行ったので他のユーザの情報の改竄を防がなくてはいけなくなりました。 Firestoreにはそれらを実装するためのセキュリティルールが存在します。
今回のセキュリティルールでは、ログイン済みの全てユーザには「allow write」によって書き込み権限を 付与し、読み込みや削除の権限は「request.auth.uid == userId」によって自分のドキュメントにだけ 付与しています。
ここで、初めてセキュリティルールを使用するにあたって不思議に思った構文があります。
パス名のところにある「{userId}」といったような記述方法です。
この記述方法はパスの「{userId}」にあたる部分が変数となりルール内の比較などに使用することができるようになります。
いまいちよくわからないですね...
実例で考えてみましょう。

上の画像は今回作成したアプリに登録したユーザのデータになります。
上のセキュリティルールにある{userId}にはこの画像にある赤枠の部分がそれぞれ入ってきます。

こうすることで、「このユーザのドキュメントはどこだ?」といった処理が可能になります。

アクセス制御

これはログアウト後のユーザがドキュメントから、自身の情報を取得しようとしたときのエラーになります。 ログアウトした場合、セキュリティルールによって権限が剥奪されるため、ドキュメントを参照することが できなくなり、エラーが発生します。ルールがしっかりと適用されている証拠ですね。

まとめ

今回は、ユーザの認証・ログイン・ログアウト・セキュリティルールの追加を行いました。 ユーザの認証面は、ドキュメントを参考にすれば問題なく実装できたのですが、セキュリティルール の追加に苦戦しました。こういう時はこうして欲しいといった考えをルール内で表現することが難しく 色々と試行錯誤して実装することができました。今回である程度コツを掴めたと思うので、 これからの作成でより、感覚を磨いていけたらなと思います。
また、今回のFirebaseとの連携方法はCocoapodによってインストールする方法を取りました。 Firestore_for_IOSで行った方法よりもはるかに 簡単でした。さらに今回はViewでの処理と、その他の処理を分ける試みも行いました。 長いコードを細かく分けると可読性も高まり、処理の流れもわかりやすくなるので、これからも 意識して分割していこうと思います。

《参考文献》
Firebaseドキュメント
https://firebase.google.com/docs/auth/ios/password-auth?hl=ja https://firebase.google.com/docs/database/security/rules-conditions?hl=ja https://firebase.google.com/docs/auth/ios/manage-users?hl=ja