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

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

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

準備

Firebase / Firestore をセッティング

公式ドキュメントにのっとればプロジェクトのセットアップまでは簡単です。

https://firebase.google.com/docs/firestore/quickstart?hl=ja

Authentication のログイン方法で、’匿名’を有効にします。

SS6.png

データベースのルールをとりあえずテストモードにしておきます。

SS2.png

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

チャットのデータの形

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

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

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

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

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

SS3.png

アプリをセッティング

公式にしたがってセットアップします。

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

ソースは

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;
    }
  }
}

『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。