Firebase / Firestore を使って簡単な Chat を作ってみる。(iOS)

最近話題の FireStore の使い方を Chat を作りながら説明してみます、その2です。
ちなみにその1は[Firebase / Firestore を使って簡単な Chat を作ってみる。(JS, Vue)](https://qiita.com/Satachito/items/33e9006a96b19b18040d)です。
今回は iOS ネイティブでやってみます。

iOS　ネイティブなので、メール等の認証ではなく、UIDevice.current.identifierForVendor を使ってやってみます。
そのために、firebase の匿名ログインの機能を使います。

# 準備

## Firebase / Firestore をセッティング

### 公式ドキュメントにのっとればプロジェクトのセットアップまでは簡単です。
https://firebase.google.com/docs/firestore/quickstart?hl=ja

### Authentication のログイン方法で、'匿名'を有効にします。
![SS6.png](https://qiita-image-store.s3.amazonaws.com/0/73493/30799d58-5658-284e-5c98-6c5a5f735dc2.png)


### データベースのルールをとりあえずテストモードにしておきます。
![SS2.png](https://qiita-image-store.s3.amazonaws.com/0/73493/0455e1a6-53e9-8bdb-4b56-c4a608670bd4.png)

Firebase / Firestore の準備はこれだけで OK です。


## チャットのデータの形

以下の4つのプロバティからなるドキュメントで、1つの投稿を表すものとします。

* body メッセージの本文
* date 投稿日
* name ハンドル（ニックネーム）
* user 投稿者の ユーザー UID 

キーは自動生成にします。

コレクションの名前は接頭詞 room- に部屋を表すキーワードをつけたものとします。

これらは決めておくだけです。SQL 型のデータベースのようにテーブル定義とかフィールド定義とかする必要はありません。

![SS3.png](https://qiita-image-store.s3.amazonaws.com/0/73493/cc723cab-f764-db4e-d974-75e1c1e53ef1.png)



## アプリをセッティング
公式にしたがってセットアップします。

* https://firebase.google.com/docs/ios/setup?hl=ja
* https://firebase.google.com/docs/firestore/quickstart?hl=ja
* https://firebase.google.com/docs/auth/ios/custom-auth?hl=ja

Podfile は以下のようになります。

```
target 'FSChat' do
  use_frameworks!

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

end
```
# iOS でやってみる


## Main.storyboard

Main.storyboard は以下のようにUITableViewController ペラ1です。
* TableHeaderView にハンドルを入力するための UITextField
* TableFooterView にメッセージを入力するための UITextField
を配置してあります。


![SS7.png](https://qiita-image-store.s3.amazonaws.com/0/73493/ba59b36d-e656-f561-ab45-3b3fff4b5e41.png)

ソースは

```Main.storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5I1-7u-cjC">
    <device id="retina5_9" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--VC-->
        <scene sceneID="uTa-sv-AN7">
            <objects>
                <tableViewController id="5I1-7u-cjC" customClass="VC" customModule="FSChat" customModuleProvider="target" sceneMemberID="viewController">
                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="VIe-S7-GOb">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        <view key="tableHeaderView" contentMode="scaleToFill" id="5ey-gZ-IFK">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
                            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                            <subviews>
                                <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Input your handle please" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Uug-qw-YcG">
                                    <rect key="frame" x="72" y="8" width="295" height="30"/>
                                    <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
                                    <nil key="textColor"/>
                                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                    <textInputTraits key="textInputTraits"/>
                                </textField>
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="You're" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mg9-gR-y16">
                                    <rect key="frame" x="8" y="12" width="56" height="21"/>
                                    <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                    <nil key="textColor"/>
                                    <nil key="highlightedColor"/>
                                </label>
                            </subviews>
                            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        </view>
                        <view key="tableFooterView" contentMode="scaleToFill" id="Inx-VL-WlJ">
                            <rect key="frame" x="0.0" y="116" width="375" height="44"/>
                            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                            <subviews>
                                <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="2lZ-Q4-hCv">
                                    <rect key="frame" x="53" y="0.0" width="314" height="30"/>
                                    <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
                                    <nil key="textColor"/>
                                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                    <textInputTraits key="textInputTraits"/>
                                    <connections>
                                        <outlet property="delegate" destination="5I1-7u-cjC" id="vLg-Vb-5PH"/>
                                    </connections>
                                </textField>
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Text:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a7l-DG-mCt">
                                    <rect key="frame" x="8" y="5" width="37" height="21"/>
                                    <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                    <nil key="textColor"/>
                                    <nil key="highlightedColor"/>
                                </label>
                            </subviews>
                            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        </view>
                        <prototypes>
                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Default" textLabel="r8v-mw-HkS" detailTextLabel="Crw-J6-bsP" style="IBUITableViewCellStyleSubtitle" id="f5a-c9-JSt">
                                <rect key="frame" x="0.0" y="72" width="375" height="44"/>
                                <autoresizingMask key="autoresizingMask"/>
                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="f5a-c9-JSt" id="Am2-re-waT">
                                    <rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
                                    <autoresizingMask key="autoresizingMask"/>
                                    <subviews>
                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="r8v-mw-HkS">
                                            <rect key="frame" x="16.000000000000004" y="5" width="33.333333333333336" height="20.333333333333332"/>
                                            <autoresizingMask key="autoresizingMask"/>
                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                            <nil key="textColor"/>
                                            <nil key="highlightedColor"/>
                                        </label>
                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Crw-J6-bsP">
                                            <rect key="frame" x="15.999999999999996" y="25.333333333333332" width="32.666666666666664" height="14.333333333333334"/>
                                            <autoresizingMask key="autoresizingMask"/>
                                            <fontDescription key="fontDescription" type="system" pointSize="12"/>
                                            <nil key="textColor"/>
                                            <nil key="highlightedColor"/>
                                        </label>
                                    </subviews>
                                </tableViewCellContentView>
                            </tableViewCell>
                        </prototypes>
                        <connections>
                            <outlet property="dataSource" destination="5I1-7u-cjC" id="PZ1-O0-XIf"/>
                            <outlet property="delegate" destination="5I1-7u-cjC" id="SX4-vB-SDb"/>
                        </connections>
                    </tableView>
                    <connections>
                        <outlet property="oHandle" destination="Uug-qw-YcG" id="JW8-dz-ZfK"/>
                    </connections>
                </tableViewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="GPu-0A-1zm" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="-378.39999999999998" y="-106.40394088669952"/>
        </scene>
    </scenes>
</document>
```

## Swift

```
Auth.auth().signInAnonymously() 
```
で匿名ログインしたあと、コレクションに　addSnapshotListener　を登録することにより、データベースの変化を取得できます。

```
self.mUnsubscribe = Firestore.firestore().collection( "room-japanese" ).order( by: "date" ).addSnapshotListener { snapshot, e in　}
```

ソースは以下のようになります。説明の都合上、1ファイルにまとめてあります。

```FSChat.swift
import UIKit
import Firebase

@UIApplicationMain class
AppDelegate: UIResponder, UIApplicationDelegate {

    var
    window: UIWindow?

    func
    application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        return true
    }
}
class
VC: UITableViewController, UITextFieldDelegate {

    var m = Array<DocumentSnapshot>()
    var mUnsubscribe: ListenerRegistration?
    
    @IBOutlet   weak    var oHandle :   UITextField!

    override func
    viewWillAppear( _ animated: Bool ) {
        super.viewWillAppear( animated )
        oHandle.text = UserDefaults.standard.string( forKey: "handle" ) ?? ""
        Auth.auth().signInAnonymously() { user, e in
            if let wE = e { print( wE ) }
            self.mUnsubscribe = Firestore.firestore().collection( "room-japanese" ).order( by: "date" ).addSnapshotListener { snapshot, e in
                if let wE = e { print( wE ) }
                if let wSnapshot = snapshot {
                    self.m = wSnapshot.documents
                    self.tableView.reloadData()
                }
            }
        }
    }
    override func
    viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear( animated )
        if let w = mUnsubscribe { w.remove() }
    }

    func
    textFieldShouldReturn( _ textField: UITextField ) -> Bool {
        guard let wUser = UIDevice.current.identifierForVendor else { abort() }
        if let wName = oHandle.text, !wName.isEmpty {
            if let wBody = textField.text, !wBody.isEmpty {
                UserDefaults.standard.set( oHandle.text, forKey: "handle" )
                UserDefaults.standard.synchronize()
                Firestore.firestore().collection( "room-japanese" ).addDocument(
                    data: [
                        "body"  : wBody
                    ,   "date"  : Int( Date().timeIntervalSince1970 * 1000 )
                    ,   "name"  : wName
                    ,   "user"  : wUser.uuidString
                    ]
                ) { e in
                    if let wE = e { print( wE ) }
                }
            }
            textField.text = ""
            textField.resignFirstResponder()
        } else {
            let wAC = UIAlertController( title: "Handle required", message: "Input your handle and try again.", preferredStyle: .alert )
            wAC.addAction( UIAlertAction( title: "OK", style: .cancel ) { _ in } )
            present( wAC, animated: true, completion: nil )
        }
        return true
    }

    override public func
    tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int {
        return m.count
    }
    
    override public func
    tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard
            let v = tableView.dequeueReusableCell( withIdentifier: "Default" )
        ,   let wTL = v.textLabel
        ,   let wDTL = v.detailTextLabel else {
            abort()
        }
        let wData = m[ indexPath.row ].data()
        wTL.text = wData[ "body" ]! as? String
        guard let wDate = wData[ "date" ]! as? TimeInterval else { abort() }
        wDTL.text = Date( timeIntervalSince1970: wDate / 1000 ).description + ":" + ( wData[ "name" ]! as? String ?? "" )
        return v
    }
}
```

# Firestore のルールの実例

Firestore のルールをテストモード（全ての読み書きを無条件で許可）にしままなのもなんなので、実際は以下のようにしてあります。

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow create: if request.auth != null;
      allow update: if false;
      allow delete: if false;
      allow list: if request.auth != null;
      allow get: if request.auth != null;
    }
  }
}
```