FirebaseでiOS版簡易SNSを作成する。(メール認証編)

# はじめに
今回、Firebaseを使ってiOS版簡易SNSを作成する記事になっています。
この記事ではアカウント作成までの部分を行なっています。

- [参考プロジェクト](https://github.com/fummicc1/SNSApp)
- [タイムライン編 前編](https://qiita.com/fummicc1-sub/items/b4975f64b9f3f2bd46ca)

# 対象者
- Xcodeがインストールされている。
- GoogleAccountを所持している。
- Cocoapodsの環境が整っている。

# やっておいてもらうこと

- Xcodeプロジェクトの作成 (当記事ではSNSAppという名前で進めていきます。)
- Firebaseプロジェクトの作成
- Cocoapodsのインストール・セットアップ

# Firebaseの事前準備

### FirebaseプロジェクトにiOSアプリを追加する。
- [ここ](https://console.firebase.google.com/)から事前に作成していただいたFirebaseのプロジェクトを選択します。

- 以下の画面のiOSを選択します。

<img width="1433" alt="スクリーンショット 2019-08-29 11.46.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/e7b89588-436c-5804-f61a-fe222cb622ce.png">

- 手順に沿って進めていきます。注意として`Bundle Identifier`はXcodeから以下の画面で確認します。
<img width="1143" alt="スクリーンショット 2019-08-29 11.49.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/34a377ed-ed49-77b4-3233-29a87430c653.png">

### CocoaPodsからFirebaseをインストールする

- Terminalを使用して自分のプロジェクトまで潜ります。その後、`pod init`と打ってください。

```sh
cd ~/Download/iPhone_Dev/SNSApp
pod init
```

- Terminalに `open Podfile`と入力します。これを打つと適切なアプリケーション(今回はテキストエディタ)で`Podfile`を開いてくれます。その後、以下のようにPodfileを編集していきます。

```ruby
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SNSApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SNSApp

pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'


end
```


```ruby
# この3行を追加する。
pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'
```

- `⌘ + S`で保存をしてテキストエディタを終了します。
- Podfileに書いたライブラリは`pod install`を行うことでインストールされます。Terminalに`pod install`と打ってください。

<img width="1680" alt="スクリーンショット 2019-08-29 11.51.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/b16316e8-f710-6247-5d66-dda76114227d.png">


- `SNSApp.xcworkspace`を開きます。

<img width="204" alt="スクリーンショット 2019-08-29 11.57.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/e41e5b33-27a2-d586-2940-b3b925b4c31e.png">

### FirebaseAuthenticationの有効化
- Web上のFirebaseコンソールから以下のように選択していきます。

<img width="1680" alt="スクリーンショット 2019-08-29 12.16.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/84abab75-d05a-0d08-2c45-449cb95c35ad.png">

<img width="1396" alt="スクリーンショット 2019-08-29 12.21.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/7f8ad882-3dab-7ce9-0cd9-a93e3c948e34.png">

<img width="1419" alt="スクリーンショット 2019-08-29 12.17.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/2646057d-5661-4b76-6bb4-5cf73da00b80.png">


<img width="1428" alt="スクリーンショット 2019-08-29 12.17.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/90b4af8c-1707-d61a-4354-052ce0cfe4be.png">


---

# Firebaseの初期設定 (Xcode編)

- iOSプロジェクトを追加した段階でやっておいてもらったFirebaseの初期設定をするコードを書きます。**既に書いた方は大丈夫**です。

```AppDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure() // ここを追加します。
        return true
    }
}
```

# モデルの作成

最初にデータをまとめる**モデル**という存在のクラスを定義します。
モデルはFirestoreにデータを書き込む際に便利になるので作った方が良いと思います。

なので、まずは**Post**モデルを作成します。

Xcodeから`File` -> `New File` -> `Swift File`を選択し、`Post.swift`というファイルを作ります。

今回保存したいデータは下の5つのデータです。

|変数名|説明|
|---|---|
|content|投稿の文章|
|postID|投稿それぞれのID|
|senderID|投稿者のUserID|
|createdAt|投稿が作成された時間|
|updatedAt|投稿が更新された時間|

さらに、**AppUser**モデルも作成しておきます。(**UserモデルだとFirebaseAuthのUserクラスとかぶる為に避けています。**)

Xcodeから`File` -> `New File` -> `Swift File`を選択し、`AppUser.swift`というファイルを作ります。

|変数名|説明|
|---|---|
|userID|FirebaseAuthのユーザーID|
|userName|ユーザーの名前|

- 次に。以下のコードをそれぞれのファイルに入力してください。

```Post.swift
import Foundation
import Firebase

struct Post {
    let content: String
    let postID: String
    let senderID: String
    let createdAt: Timestamp
    let updatedAt: Timestamp
    
    init(data: [String: Any]) {
        content = data["content"] as! String
        postID = data["postID"] as! String
        senderID = data["senderID"] as! String
        createdAt = data["createdAt"] as! Timestamp
        updatedAt = data["updatedAt"] as! Timestamp
    }
}
```

**Timestamp**は場所に依存しない時間であるそうで、記述するには`import Firebase`を書く必要があります。

```User.swift
import Foundation
import Firebase

struct AppUser {
    let userID: String
    let userName: String
    
    init(data: [String: Any]) {
        userID = data["userID"] as! String
        userName = data["userName"] as! String
    }
}
```

Codableを使用してもいいのですが、今回は簡易的に`[String: Any]`を手動でパースしています。

# TimelineViewController.swiftをコーディング
- Xcodeから`File` -> `New File` -> `Cocoa Touch Class`を選択し、`TimelineViewController`というクラスを作ります。

- 中身は以下のように編集しておきます。この`me`はアカウント登録成功後に前の画面(`AccountViewController`)から値渡しで受け取る変数です。`me`から連想できるように私自身のユーザーデータを表しています。

```TimelineViewController.swift
import UIKit
import Firebase // 追加

class TimelineViewController: UIViewController {

    var me: AppUser! // 追加
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
```

# AccountViewController.swiftをコーディング
- Xcodeから`File` -> `New File` -> `Cocoa Touch Class`を選択し、`AccountViewController`というクラスを作ります。

- UIKitの宣言・デリゲートメソッドの記述を行います。

```swift
import UIKit
import Firebase // Firebaseをインポート

class AccountViewController: UIViewController {
    
    @IBOutlet var emailTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }
    
　  // 登録ボタンを押したときに呼ぶメソッド。
    @IBAction func registerAccount() {
        
    }
}

// デリゲートメソッドは可読性のためextensionで分けて記述します。
extension AccountViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}
```

いよいよ、Firebaseを使用してユーザーを作成していきます。

- プロパティとして、FirebaseAuthのクラスを宣言します。

```AccountViewController.swift
import UIKit
import Firebase

class AccountViewController: UIViewController {
    
    (省略)
    var auth: Auth // 追加

    override func viewDidLoad() {
        super.viewDidLoad()
        auth = Auth.auth() // 追加
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

    (省略)

}
```

次に、アカウント登録する処理を書いていきます。
- `registerAccount`というメソッドを定義してユーザーがボタンを押した時にアカウント登録を行うような実装をします。

```アカウント作成のコード.swift
auth.createUser(withEmail: "メールアドレス", password: "パスワード") { (result, error) in
    // アカウント登録後に呼ばれる。
    // error変数が nil -> 成功
    //            nilではない -> 失敗
    // result変数 ... user情報などをプロパティとして格納している。 
}
```

実装したコードとしては、

```AccountViewController.swift
import UIKit
import Firebase

class AccountViewController: UIViewController {

    (省略)

    @IBAction func registerAccount() {
        let email = emailTextField.text!
        let password = passwordTextField.text!
        auth.createUser(withEmail: email, password: password) { (result, error) in
            if error == nil, let result = result {
                // errorが nil であり、resultがnilではない == user情報がきちんと取得されている。
                self.performSegue(withIdentifier: "Timeline", sender: result.user)　// 遷移先の画面でuser情報を渡している。
            }
        }
    }
}
```



# Main.storyboardを編集

- UITextFieldを２つとUIButtonを１つずつ用意する。



- ViewControllerのクラスを`AccountViewController`に変更する。

<img width="1228" alt="スクリーンショット 2019-08-29 12.57.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/fcc6fae9-dc7e-0064-565b-3e557a80a624.png">

- 関連付けを行う。

<img width="1226" alt="スクリーンショット 2019-08-29 12.57.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/ffd94a7c-3883-89e5-8b67-5e331c195f91.png">


- 新しくViewControllerを作成して、クラスを`TimelineViewController`に設定する。

- セグエを`AccountViewController`→`TimelineViewController`に繋げて作成する。

- セグエのidentifierを`Timeline`に設定する。
<img width="1225" alt="スクリーンショット 2019-08-29 13.01.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/1cc3eeec-729b-5c5e-4e9c-f0099c7a00c5.png">

現在、このようにアカウントを作成するところまで進めました。

![demo2](https://media.giphy.com/media/j5JPSoBAIYSTkbGLDz/giphy.gif)

# AccountViewController.swiftを編集

- **一度ログインしていたらアカウント作成画面を飛ばす。**
- **メール認証機能を実装する。**

この２つを最後に実装していきます。

---

### 一度ログインしていたらアカウント作成画面を飛ばす。

```AccountViewController.swift
import UIKit
import Firebase

class AccountViewController: UIViewController {
    
    var auth: Auth!
    
    (省略)

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if auth.currentUser != nil {
            // もし既にユーザーにログインができていれば、タイムラインの画面に遷移する。
            // このときに、ユーザーの情報を次の画面の変数に値渡ししておく。(直接取得することも可能。)
            performSegue(withIdentifier: "Timeline", sender: auth.currentUser!)
        }
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let nextViewController = segue.destination as! TimelineViewController
        let user = sender as! User
        nextViewController.me = AppUser(data: ["userID": user.uid])
    }
    
    @IBAction func registerAccount() {
        let email = emailTextField.text!
        let password = passwordTextField.text!
        auth.createUser(withEmail: email, password: password) { (result, error) in
            if error == nil, let result = result {
                self.performSegue(withIdentifier: "Timeline", sender: result.user)
            }
        }
    }
}
```

ポイントは、

```swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if auth.currentUser != nil {
        performSegue(withIdentifier: "Timeline", sender: auth.currentUser!)
    }
}
```

- **viewDidAppear**で 既にユーザーを所持している == ログインしたことがある ならば、`performSegue`で画面遷移を実行しています。

- senderにユーザー情報を渡していますが、これは最新の状態を`Auth.auth().currentUser`で取得できるのでこれの限りではないです。

また、`AppUser`を扱っているので、注意が必要です。`nextViewController.me = user`としないようにしてください。

```swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let nextViewController = segue.destination as! TimelineViewController
    let user = sender as! User
    nextViewController.me = AppUser(data: ["userID": user.uid])
}
```

- `prepare`はセグエによる画面遷移が行われる際に呼ばれます。

- この実装で、既にログインを行なっていれば、画面遷移時が行われ、サインアップをする画面をスキップできます。

![demo1](https://media.giphy.com/media/QX11UM4PggtcgnRKx3/giphy.gif)

---

### メール認証機能を実装する。

- ここでいうメール認証機能とは **入力したメールアドレス宛に確認メールがFirebaseから送られてきて、それに応じれば正しいアカウントと見なせる** という意味でのメール認証機能です。

コード全体です。

```swift
import UIKit
import Firebase

class AccountViewController: UIViewController {
    
    var auth: Auth!
    
    @IBOutlet var emailTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        auth = Auth.auth()
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if auth.currentUser != nil {
            auth.currentUser?.reload(completion: { error in
                if error == nil {
                    if self.auth.currentUser?.isEmailVerified == true {
                        self.performSegue(withIdentifier: "Timeline", sender: self.auth.currentUser!)
                    } else if self.auth.currentUser?.isEmailVerified == false {
                        let alert = UIAlertController(title: "確認用メールを送信しているので確認をお願いします。", message: "まだメール認証が完了していません。", preferredStyle: .alert)
                        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                        self.present(alert, animated: true, completion: nil)
                    }
                }
            })
        }
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let nextViewController = segue.destination as! TimelineViewController
        let user = sender as! User
        nextViewController.me = AppUser(data: ["userID": user.uid])
    }
    
    @IBAction func registerAccount() {
        let email = emailTextField.text!
        let password = passwordTextField.text!
        auth.createUser(withEmail: email, password: password) { (result, error) in
            if error == nil, let result = result {
                result.user.sendEmailVerification(completion: { (error) in
                    if error == nil {
                        let alert = UIAlertController(title: "仮登録を行いました。", message: "入力したメールアドレス宛に確認メールを送信しました。", preferredStyle: .alert)
                        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                        self.present(alert, animated: true, completion: nil)
                    }
                })
            }
        }
    }
}

// デリゲートメソッドは可読性のためextensionで分けて記述します。
extension AccountViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}
```

ポイントは、

```swift
auth.createUser(withEmail: email, password: password) { (result, error) in
    if error == nil, let result = result {
        result.user.sendEmailVerification(completion: { (error) in
            if error == nil {
                let alert = UIAlertController(title: "仮登録を行いました。", message: "入力したメールアドレス宛に確認メールを送信しました。", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                self.present(alert, animated: true, completion: nil)
            }
        })
    }
}
```

ここの部分の

```swift
result.user.sendEmailVerification(compeltion: @escaping (Error?) -> ())
```

で入力したメールアドレスに対して確認用メールを送信します。

- コールバックで指定されている引数は確認用メールの送信に成功していれば `nil` で失敗していれば `Error型` が入っています。

- もしエラーがnilであればメールボックスを確認してもらうようにユーザーに促すようにアラートを表示します。

![demo3](https://media.giphy.com/media/U6MlODj35rLwNpp7D1/giphy.gif)

次に、メールボックスから送信されたメールを見つけ、リンクを開きます。

![demo4](https://media.giphy.com/media/Vi05aPeFKxe5GgkbW6/giphy.gif)

その後、アプリをもう一度開くと、画面遷移が行われるはずです。

![demo5](https://media.giphy.com/media/gHhZHnirnHEp4d4JoK/giphy.gif)

コードの説明に戻りますが、


```swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if auth.currentUser != nil {
        auth.currentUser?.reload(completion: { error in
            if error == nil {
                if self.auth.currentUser?.isEmailVerified == true {
                    self.performSegue(withIdentifier: "Timeline", sender: self.auth.currentUser!)
                } else if self.auth.currentUser?.isEmailVerified == false {
                    let alert = UIAlertController(title: "まだメール認証が完了していません。", message: "確認用メールを送信しているので確認をお願いします。", preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                    self.present(alert, animated: true, completion: nil)
                }
            }
        })
    }
}
```

ここの部分で、ポイントは

```swift
self.auth.currentUser?.isEmailVerified
```
- これで現在のユーザーがメール認証済みかのチェックが可能です。

- また、**メール認証を実際に行なったかをアプリ側で自動更新はしてくれない**ので、以下のメソッドを呼んでわざと更新処理を行います。

```swift
auth.currentUser?.reload(completion: @escaping (Error?) -> ())
```

- その後`if self.auth.currentUser?.isEmailVerified == true`のようにif文でチェックします。

# 最後に

この記事では、

- FirebaseAuthを利用してアカウント登録を行う。
- ユーザーログイン済みの場合、勝手に画面遷移を行わせることでUXの向上
- 確認用メールを送信

この3つを実装しました。

[次の記事](https://qiita.com/fummicc1-sub/items/b4975f64b9f3f2bd46ca)では続編としてFirestoreを使用して、タイムライン画面を作成していきます。