【Swift】【Firebase】chatアプリ作ってたらtodoアプリになった件について【Firestore】【iOS】

# □ はじめに
いつかネイティブアプリを作ってみたいと思っていて、やっと手を出せました。今回はiOSアプリをとにかく作ってみるのに注力しました。

Firebaseも触ってみたい気持ちもあったので、[Firebaseで1時間で簡単なWebチャットアプリが作れるハンズオン](https://qiita.com/taketakekaho/items/52b7c196ddbd4cb3c968)のサンプルアプリとして作ったwebアプリをiOSネイティブアプリにしてみようと思います。

本当に初めてなので色々苦戦しました。初心者の方の参考になればと。

# □ 環境
* Swift version 5.1
* Xcode version 11.1

# □ 完成アプリ
最初はサンプルのchatアプリにするつもりだったんですが、結果todoアプリみたいなものになりました。

![ac33e4f51632402c1e8ed6e49cdbef09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/4322d4b0-744e-0a21-f4e5-dad85e5908d4.gif)

# □ アウトライン
本編の流れとしては下記のようにします。
1. 配置する部品
2. コードとの紐付け
3. cloud firestoreとの接続
4. 実機テスト


技術ポイントとしては既出のものばかりですが下記の要素をまとめていこうと思います。

* swiftの基本動作
* tableViewCellのカスタマイズ
* キーボード出現時の画面スライド
* firestoreへの追加/更新/削除

## 1. 配置する部品
### Main.storybord
storybordはこんな感じです。
inputTextはメッセージ入力用,SENDはメッセージ送信用,TableViewはメッセージをスタックする用, meesageListはスタックするセルリスト用に準備しました。

![スクリーンショット 2019-10-14 14.25.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/3d70949d-87d5-83a3-a4e8-c19b5ef95c16.png)

TableViewCellのstyleはCustomで作成しました。
「File」 → 「new」 → 「iOS」の「Cocoa Touch Class」 → 「class:クラス名, subclass of:UITableViewClass, Also create XIB fileにチェックを入れる」 → 「Next」
これでクラス名.swiftとクラス名.xibの2つのファイルが作成されます。この2つのファイルを使ってスタックするセルを作成します。

![スクリーンショット 2019-10-14 16.07.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/1cf1ce97-5f49-4438-1c4d-fd1294516e66.png)
![スクリーンショット 2019-10-14 17.01.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/32569c5b-94d3-2c3e-a9a9-a4ad35ac7eac.png)


今回はクラス名はCustomCellTableViewCellです。

### CustomCellTableViewCell.xib

CustomCellTableViewCell.xibはシンプルにアイコン用のimageViewとメッセージ用のlabelを配置します。
サンプルアプリのように1ユーザーが1メッセージを反映する用です。

![スクリーンショット 2019-10-14 16.12.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/7cb53e0f-ae0a-f907-18f6-09fd3b26ebd4.png)

配置する部品については以上です。

## 2. コードとの紐付け
作業がしやすいようにstoryboardとコードが1画面に分割されるように表示させます。
(storyboardを表示させた状態でoption押しながらViewController.swiftをクリック)

![スクリーンショット 2019-10-14 20.53.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/ee840599-5484-40ca-e869-3355eba1ca42.png)


### メッセージ入力と出力
inputTextをOutlet,SENDをActionとして紐付けます。
(controlを押しながら部品名をコードへドラッグ&ドロップ)
ひとまず、メッセージを入力してSENDボタンを押下することで入力したメッセージをコンソールに出力します。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var inputText: UITextField!

    @IBAction func sendText(_ sender: UIButton) {
        print(inputText.text!)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}
```

しかし、これだとキーボードが出現したら入力フィールドが隠れてしまうし、キーボードが閉じません。

### キーボード出現時の画面スライドとクローズ
このサイトを参考にしながら下記のように実装しました。
https://note.mu/ichirou_okada/n/n630563aea246

ほぼ転記ですが、fixしないと動かない点がありましたので一応コードを載せておきます。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    
    @IBOutlet weak var inputText: UITextField!

    @IBAction func sendText(_ sender: UIButton) {
        print(inputText.text!)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        inputText.delegate = self
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.configureObserver()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.removeObserver() // Notificationを画面が消えるときに削除
    }

    // Notificationを設定
    // 名称が変わっているのに加えて,fixが2箇所必要でした
    func configureObserver() {
        let notification = NotificationCenter.default
        notification.addObserver(self,
        selector: #selector(keyboardWillShow(notification:)),
        name: UIResponder.keyboardWillShowNotification,
        object: nil)
        notification.addObserver(self,
        selector: #selector(keyboardWillHide(notification:)),
        name: UIResponder.keyboardWillHideNotification,
        object: nil)
    }

    // Notificationを削除
    func removeObserver() {
        let notification = NotificationCenter.default
        notification.removeObserver(self)
    }

    // キーボードが現れた時に、画面全体をずらす。
    // @objcを加える
    @objc func keyboardWillShow(notification: Notification?) {
        let rect = (notification?.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
        let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        UIView.animate(withDuration: duration!, animations: { () in
            let transform = CGAffineTransform(translationX: 0, y: -(rect?.size.height)!)
            self.view.transform = transform
        })
    }

    // キーボードが消えたときに、画面を戻す
    // @objcを加える
    @objc func keyboardWillHide(notification: Notification?) {
        let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
        UIView.animate(withDuration: duration!, animations: { () in
            self.view.transform = CGAffineTransform.identity
        })
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder() // Returnキーを押したときにキーボードを下げる
        return true
    }
}
```

これでキーボード出現と同時に画面がスライドして、returnを押すとキーボードが閉じます。
これでinputTextにキーボードがかぶらず入力できるかと思います。

### CustomセルのViewへの反映
まずTableViewをstoryboard上ViewControllerのdataSource,delegateと紐付け、さらにコード上にtableViewとして紐付けします。

![スクリーンショット 2019-10-16 0.00.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/c49b2a0e-b2bc-22cd-603f-97569599f01c.png)

下記のようにUITableViewDeletegateとUITableViewDataSourceを継承させます。すると、エラーが出ると思うのfixをクリックしてください。tableViewメソッドを実装しろって感じになるので以下のように実装します。

同時に表示するメッセージ情報を格納しておくString配列messageListも宣言しておきます。

また、sendTextメソッドを書き換えます。tableViewはmessageListの内容を反映しているため、sendTextメソッドでmessageListに情報を追加します。これでmessageList.countで返すcell数がmessageListの長さになります。さらにtableView.reloadDataでリロードします。

```swift:ViewController.swift
import UIKit

// UITableViewDelegate, UITableViewDataSourceを追加する
class ViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource {

    // メッセージ格納用のString配列を宣言
    var messageList: [String] = []
    @IBOutlet weak var inputText: UITextField!
    // TableViewを紐付け
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // tableViewに自分で定義したCustomCellをreuseCellという名前で登録します
        tableView.register(UINib(nibName: "CustomCellTableViewCell", bundle: nil), forCellReuseIdentifier:"reuseCell")
        inputText.delegate = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // TableViewにいくつCellを表示させるかの数値を返します
        return messageList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 表示させるcellをreuseCellにします, CustomCellTableViewCell型にキャストします
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseCell", for: indexPath) as!
        CustomCellTableViewCell
        let message = messageList[indexPath.row]
        // CustomCellで実装した表示用のメソッドを呼び出します(この後、実装します)
        cell.cellShow(text: message)
        return cell
    }

    @IBAction func sendText(_ sender: UIButton) {
        // SENDボタンを押したらmessageListに入力したmessageを追加
        // self.tableView.reloadData()でtableViewをリロードするとmessageListの内容が反映されます
        messageList.append(inputText.text!)
        self.tableView.reloadData()
    }


```


次にCustomCellTableViewCell.swiftを実装します。labelとimageViewをコード上に紐付けて、それぞれのフィールドに値を代入するメソッドを実装します。今回はcellShowメソッドとします。String型のmessageを受け取り、label.textに入れてimages.imageに表示する画像名を指定します。今回はiconとします(下記で画像の追加方法を説明します。)


```swift:CustomCellTableViewCell.swift

import UIKit

class CustomCellTableViewCell: UITableViewCell {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var images: UIImageView!
    
    func cellShow(message: String) {
        self.label.text = message
        self.images.image = UIImage(named: "icon")
    }
}

```

### 画像の追加
Assets.xcassetsを開きます。下部にある+ボタンを押して、「new Image Set」を選択します。AppIconの下に「image」ができるので名前を「icon」に変更します。

![スクリーンショット 2019-10-18 13.02.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/a92bf989-fcdc-c121-80a5-866a15060265.png)

次にAssets.cassetsフォルダを開きます。すると、icon.imagesetができるのでそこにiconとして使用する画像を移します。

![スクリーンショット 2019-10-18 13.03.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/3dabb60e-40bd-53db-14ac-84cfabbecd68.png)

最後にxcodeに戻り、Assets.xcassetsを開きiconをクリックします。先ほどフォルダに移した画像がUnssignedになっていると思うので1xに移動します。これで画像をiconとして登録することができました。

![スクリーンショット 2019-10-18 13.09.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/f1c15c12-16af-ef05-52cb-0869ef0df61f.png)

### シミュレーション
ここでシミュレーションをすると以下のような挙動になるかと思います。

![idjux-4hkgd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/e796de8d-da32-077e-aa4f-ee8e951a8f8b.gif)



SENDボタンを押すと入力したメッセージがmessageListに入り、tableView.reloadDataによって画面に反映されます。
ありきたりなサンプルアプリですが、webアプリをiOSネイティブアプリで作り直すはだいたい達成できたかと思います。

# □ 最後に
長くなってしまったので次の記事でバックエンドにfirebaseを利用した実装を書こうと思います。
まずはswiftを触ってみるという目標は達成できたので良かったです。

書いてみると思ったよりも簡単で食わず嫌いしていた期間がもったいなかったです(笑)
今後はswift-uiがデフォルトになるらしいのでそちらの書き方も学んでいきたいです。

ここまでの実装も載せておきます。

```swift:ViewController.swift

import UIKit

class ViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messageList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseCell", for: indexPath) as!
        CustomCellTableViewCell
        let messageTitle = messageList[indexPath.row]
        cell.cellShow(text: messageTitle)
        return cell
    }
    
    var messageList: [String] = []
    @IBOutlet weak var inputText: UITextField!
    @IBOutlet weak var tableView: UITableView!
    
    
    @IBAction func sendText(_ sender: UIButton) {
        messageList.append(inputText.text!)
        print(messageList)
        self.tableView.reloadData()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "CustomCellTableViewCell", bundle: nil), forCellReuseIdentifier:"reuseCell")
        inputText.delegate = self
    }
    
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.configureObserver()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.removeObserver() // Notificationを画面が消えるときに削除
    }

    // Notificationを設定
    func configureObserver() {
        let notification = NotificationCenter.default
        notification.addObserver(self,
        selector: #selector(keyboardWillShow(notification:)),
        name: UIResponder.keyboardWillShowNotification,
        object: nil)
        
        NotificationCenter.default.addObserver(self,
        selector: #selector(keyboardWillHide(notification:)),
        name: UIResponder.keyboardWillHideNotification,
        object: nil)
    }

    // Notificationを削除
    func removeObserver() {

        let notification = NotificationCenter.default
        notification.removeObserver(self)
    }

    // キーボードが現れた時に、画面全体をずらす。
    @objc func keyboardWillShow(notification: Notification?) {

        let rect = (notification?.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
        let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        UIView.animate(withDuration: duration!, animations: { () in
            let transform = CGAffineTransform(translationX: 0, y: -(rect?.size.height)!)
            self.view.transform = transform

        })
    }

    // キーボードが消えたときに、画面を戻す
    @objc func keyboardWillHide(notification: Notification?) {

        let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
        UIView.animate(withDuration: duration!, animations: { () in

            self.view.transform = CGAffineTransform.identity
        })
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {

        textField.resignFirstResponder() // Returnキーを押したときにキーボードを下げる
        return true
    }
}
```

# □ 参考
[Swiftで始めるFirebase入門](https://qiita.com/Simmon/items/dc3490be59acea7fbe0c)
[Firebaseで1時間で簡単なWebチャットアプリが作れるハンズオン](https://qiita.com/taketakekaho/items/52b7c196ddbd4cb3c968)
[SwiftでTableViewを使ってみよう](https://qiita.com/TD3P/items/cafa8e20029047993025)