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