最近話題の FireStore の使い方を Chat を作りながら説明してみます、その2です。
ちなみにその1はFirebase / Firestore を使って簡単な Chat を作ってみる。(JS, Vue)です。
今回は iOS ネイティブでやってみます。
iOS ネイティブなので、メール等の認証ではなく、UIDevice.current.identifierForVendor を使ってやってみます。
そのために、firebase の匿名ログインの機能を使います。
準備
Firebase / Firestore をセッティング
公式ドキュメントにのっとればプロジェクトのセットアップまでは簡単です。
Authentication のログイン方法で、'匿名'を有効にします。
データベースのルールをとりあえずテストモードにしておきます。
Firebase / Firestore の準備はこれだけで OK です。
チャットのデータの形
以下の4つのプロバティからなるドキュメントで、1つの投稿を表すものとします。
- body メッセージの本文
- date 投稿日
- name ハンドル(ニックネーム)
- user 投稿者の ユーザー UID
キーは自動生成にします。
コレクションの名前は接頭詞 room- に部屋を表すキーワードをつけたものとします。
これらは決めておくだけです。SQL 型のデータベースのようにテーブル定義とかフィールド定義とかする必要はありません。
アプリをセッティング
公式にしたがってセットアップします。
- 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
を配置してあります。
ソースは
<?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ファイルにまとめてあります。
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;
}
}
}