Firebase 教程 —— 一個實時聊天室
原文是基于Firebase 2.X構筑的匿名聊天室Demo,我在根據原文構筑時FireBase已經更新到3.X,谷歌對Firebase SDK做了不小的改動,所有的功能都由不同的類來操作,不再由Firebase類統一調度,這些改動致使原文某些地方變得不合時宜。
因此,我將自己根據原文構筑的基于3.X Firebase的流程及所遇到的坑穿插在原文中并且重新構筑了原文的部分代碼。(有沒有覺得構筑這兩個字很有逼格?!)
現在主流的 App 都開始支持聊天功能了——你的 App 是不是也該支持一下?
但是,制作一個聊天工具確實不是一件簡單的任務。我們不但缺乏現成的專門針對聊天的 UIKit 組件,還需要一個服務器來負責處理用戶間的消息及對話。
幸運的是,我們可以使用一個優秀的框架:Firebase。它能為我們同步實時數據而無需編寫任何服務端代碼,同時還提供一個 JSQMessagesViewController 用于顯示消息,這個 UI 可以和本地的消息應用相媲美。
在這個 Firebase 教程中,我們會創建一個可以進行匿名聊天的 App 叫做 ChatChat,如下圖所示:
最終你將學習到:
1、用 CocoaPods 安裝 Firebase SDK 和 JSQMessagesViewController。
2、用 Firebase 數據庫來同步實時數據。
3、讓 Firebase 支持匿名登錄。
4、用 JSQMessagesViewController 實現 UI。
5、當用戶進行輸入時,進行提示。
好了,接下來就開始吧!
開始
在開始本教程之前,請下載 開始項目 ,目前,它只完成了一個假的登錄界面。
我們可以用CocoaPods安裝Firebase SDK 和JSQMessagesViewController。如果你對 CocoaPods 不熟,可以參考我們的 CocoaPods Swift 教程 。
打開終端,進入項目文件夾路徑。在項目根路徑下,新建一個 Podfile 文件。在文件中加入對 Firebase SDK 和 JSQMessagesViewController 的依賴,如下所示:
platform :ios, "9.0"
use_frameworks!
target 'ChatChat' do
pod 'Firebase'
pod 'JSQMessagesViewController'
end
保存 Podfile 文件,然后用以下命令安裝依賴:
pod install
譯者注:由于眾所周知的原因,CocoaPods 對于國內用戶來說并不友好,經常出現各種無法 pod install 的情況。如果是這樣,你必須手動安裝這兩個庫了。關于 Firebase 的手動安裝,請看這里。關于 JSQMessagesViewController 的手動安裝,你需要從 github 下載 JSQMessagesViewController 和 JSQSystemSoundPlayer 這兩個庫的源文件然后添加到項目里,并改正 JSQSystemSoundPlayer+JSQMessages.m 中的兩個錯誤即可,然后在橋接頭文件中導入相應的 .h 文件(包括并不限于):
#import "JSQMessage.h"
#import "JSQMessagesBubbleImage.h"
#import "JSQMessagesViewController.h"
#import "JSQMessagesBubbleImageFactory.h"
#import "UIColor+JSQMessages.h"
#import "JSQMessageAvatarImageDataSource.h"
#import "JSQSystemSoundPlayer+JSQMessages.h"
茄子注:個人使用CocoaPod安裝FireBase時慘遭谷歌翻臉,然后在手動集成時又被官方文檔坑了,在此將手動集成時的一些注意點根據自己手動集成的流程說明。
首先,你可以在 這里 下載Firebase SDK
選擇其中一些組件或者像我一樣直接把整個包丟進項目中。
接著第三步,將ObjC鏈接器標志添加到Other Linker Settings中。
這一步需要注意的是,你需要在Project中及使用到Firebase的Targets中都將ObjC鏈接器標志添加到Other Linker Settings中。
接著,Run一下,是否Crash及控制臺報告如下:
Configuring the default app.
<FIRAnalytics/DEBUG> Debug mode is on
<FIRAnalytics/INFO> Firebase Analytics v.3301000 started
<FIRAnalytics/INFO> To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://goo.gl/Y0Yjwu)
<FIRAnalytics/DEBUG> Debug logging enabled
<FIRAnalytics/DEBUG> Monitoring the network status
Firebase Crash Reporting: Successfully enabled
//導致Crash的元兇
A reversed client ID should be added as a URL scheme to enable Google sign-in.
我們需要將注冊FireBase項目時獲得的GoogleService-Info.plist中間中的REVERSED_CLIENT_ID添加到Info -> URL Types -> URL Schemes中,如下圖:
再次運行你就能看到正常的運行畫面:
注意:在接下來的教程中,你每次編譯和運行都會看到這個界面。點擊“匿名登錄”又會切換到另一個界面。目前點擊按鈕沒有什么用處,但隨后我們就會實現它。
如果你第一次接觸 Firebase,你需要創建一個賬號。不用擔心—— 它非常簡單,而且完全是免費的,不需要信用卡。
注意:關于如何注冊 Firebase 的完整步驟,請看我們的 Firebase 入門教程 。
注冊 Firebase 賬號
進入 Firebase 注冊頁面 ,創建一個賬號,然后創建一個 Firebase App。就本教程而言,你需要使用實時數據庫和身份認證服務。
開啟匿名認證
Firebase 允許用戶通過 email 地址或社交賬號進行登錄,但也提供匿名登錄功能,后者會給每個用戶分配一個唯一的 ID 但不需要用戶的輸入任何個人信息。
匿名認證就好比說:“我不知道你是誰,我只知道你是一個人。”。對于訪問賬戶或者使用用戶來說,這是非常方便的。這對于本教程來說非常適合,因為 ChatChat 中所有用戶都是匿名的。
要開啟匿名認證,你需要進入你的 Firebase 項目 ,選擇 Auth 標簽,若是初次創建則會指引你選擇登陸方法,啟用匿名登陸即可。
這樣,你就開啟了超級隱身模式,也就是匿名認證——很爽吧:]
登陸
打開 LoginViewController.Swift,加入:
import Firebase
要登入聊天室,需要連接到 Firebase 數據庫。在 LoginViewController.swift 中加入:
class LoginViewController: UIViewController {
var ref: FIRDatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().referenceFromURL("https://fir-demo-43879.firebaseio.com/")
}
上面的代碼是什么意思?
首先,定義一個屬性,存放 Firebase database 的引用。
然后,用你的 Firebase App URL 創建一個 Firebase 數據庫連接并賦給這個屬性。
如果你不知道你的 Firebase App URL 是什么,就像上一步一樣點擊Database即可 :
茄子注,雖然Firebase沒有向我們提供Database的源代碼,但從其API上大致可以猜測一二,使用URL進行首次連接,接著在注銷之前便會無限制地互發數據,這點上與WebSocket的機制非常相似,我們有理由猜測FireDatabase的底層是利用WebSocket的概念實現的。
要登錄一個用戶,可以在數據庫引用對象上調用 authAnonymouslyWithCompletionBlock(_:)。
在 loginDidTouch(_:) 方法中添加代碼:
@IBAction func loginDidTouch(sender: AnyObject) {
FIRAuth.auth()?.signInAnonymouslyWithCompletion({ (user, error) in
if error != nil { print(error?.description); return }
print(user?.uid)
self.performSegueWithIdentifier("LoginToChat", sender: nil) // 3
})
}
在這個方法中,完成了如下工作:
1、調用 FiRAuth的單例 的 authAnonyouslyWithCompletionBlock(_:) 方法以匿名方式登錄一個用戶。
2、檢查是否認證失敗。
3、在閉包中,調用 segue 跳轉到 ChatViewController。
閉包回調中的user包含了用戶的唯一標示符及是否匿名登陸等重要信息,當我們在使用到時可以通過:
FIRAuth.auth()?.currentUser
來獲得相應信息。
茄子注:匿名登陸并不會隨時改變,就像一般的賬戶一樣通過一個refreshToken來實現長期登陸,這個屬性被加密成無規則字符串。
創建聊天界面
JSQMessagesViewController 是一個 UICollectionViewController 的封裝,為聊天進行了專門的定制。
本教程將主要介紹 5 個步驟:
1、創建消息數據
2、創建帶背景色的消息氣泡
3、刪除頭像
4、改變 UICollectionViewCell 的文字顏色
5、提示用戶正在輸入
幾乎每個步驟都需要覆蓋一些方法。JSQMessagesViewController 使用了JSQMessagesCollectionViewDataSource 協議,因此我們需要覆蓋協議的默認實現就可以了。
注意:關于 JSQMessagesCollectionViewDataSource 的更多內容,請參考 這里 。
打開 ChatViewController.swift 導入 Firebase 和JSQMVC :
import Firebase
import JSQMessagesViewController
將父類從 UIViewController 類修改 JSQMessagesViewController 類:
class ChatViewController: JSQMessagesViewController {
現在 ChatViewController 繼承了 JSQMessagesViewController,我們可以設置 senderId 和 senderDisplayName 的初始值了,這樣 App 才能唯一識別消息的發送者——否則它無從知道發送者是誰。
在 LoginViewController 中,我們用 user 將用戶信息傳遞給 ChatViewController(通過 prepareForSegue 方法)。
在 LoginViewController 中添加方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
guard let naV = segue.destinationViewController as? UINavigationController else { return }
guard let chatVC = naV.viewControllers.first as? ChatViewController else { return }
chatVC.senderId = FIRAuth.auth()?.currentUser?.uid
chatVC.senderDisplayName = ""
}
在上面的代碼中:
1、獲取 segue 的目標 View Controller 并轉換成 UINavigationController。
2、將 Navigation Controller 的第一個 View Controller 轉換成ChatVC。
3、將本地用戶的 ID 賦給 chatVc.senderId,這是 JSQMVC 用于處理消息的客戶端 ID。
4、將 chatVc.senderDisplayName 設為空字符串,因為我們的聊天室是匿名登錄的。
注意每個 App 會話中,我們只會有一個匿名的會話。每次重啟 App 之后,你都會獲得一個新的、唯一的匿名用戶。如果你重啟模擬器,你會看到另外一個用戶 ID。
茄子注:上面那段話存在于原譯文之中,我沒有與英文原文核實是否英文原文存在。有可能是Firebase的機制變化了,我們可以從不斷重啟模擬器、打印user.uid及通過Firebase項目的Auth頁面查看驗證得到user.uid沒有發生變化,因此每次重啟App之后都會得到一個新的匿名用戶是不正確的。
運行程序,檢查你的 App 是否運行在超級隱身模式:
通過簡單地繼承下 JSQMessagesViewController,你就獲得了一個完整的聊天 UI。太爽了!
創建數據源及委托
現在,我們有了一個聊天 UI,你可能很想在上面顯示點什么了。但首先,你需要注意幾件事情。
要顯示聊天消息,我們需要一個數據源,即一個實現了 JSMessageData 協議的對象并實現一些委托方法。我們可以自己定義一個類來實現 JSQMessageData 協議,也可以使用現成的 JSQMessage 類。
在 ChatViewController 頭部,定義一個屬性:
// MARK: Properties
var messages = [JSQMessage]()
messages 屬性是一個數組,存儲了多個 JSQMessage 實例。
在 ChatViewController 中,實現 2 個委托方法:
override func collectionView(collectionView: JSQMessagesCollectionView!,
messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return messages.count
}
這兩個委托方法可能并不陌生。第一個方法和 collectionView( :cellForItemAtIndexPath:) 方法一樣,只不過返回類型變成了 JSQMessageData 而已。第二個則和 collectionView( :numberOfItemsInSection:) 方法完全一樣。
還有幾個必須實現的委托方法,用于提供消息數據、氣泡圖片以及頭像。提供消息數據的方法已經實現了,接下來就是提供氣泡和頭像的方法。
氣泡顏色
在 Collection View 中,消息文本顯示在一個簡單的圖片背景之上。有兩種類型的消息:收到的消息和發出的消息。發出的消息靠右側顯示而收到的消息則靠左顯示。
在 ChatViewController 中,添加兩個屬性:
var outgoingBubbleImageView: JSQMessagesBubbleImage!
var incomingBubbleImageView: JSQMessagesBubbleImage!
然后添加方法:
private func setupBubbles() {
let factory = JSQMessagesBubbleImageFactory()
outgoingBubbleImageView = factory.outgoingMessagesBubbleImageWithColor(
UIColor.jsq_messageBubbleBlueColor())
incomingBubbleImageView = factory.incomingMessagesBubbleImageWithColor(
UIColor.jsq_messageBubbleLightGrayColor())
}
JSQMessagesBubbleImageFactory 有創建聊天氣泡的方法。在 JSQMessagesViewController 中有一個 Category,允許我們使用原生消息 App 中消息氣泡所使用的顏色。
通過 bubbleImageFactory.outgoingMessagesBubbleImageWithColor() 和 bubbleImageFactory.incomingMessagesBubbleImageWithColor() 方法,我們可以創建出接收消息和發出消息的氣泡圖片。
然后,在 viewDidLoad() 方法中調用這個 setupBubbles() 方法:
override func viewDidLoad() {
super.viewDidLoad()
title = "ChatChat"
setupBubbles()
}
設置氣泡圖片
要為每條消息設置顏色氣泡,我們需要覆蓋 JSQMessagesCollectionViewDataSource 協議中的一個方法。
collectionView(_:messageBubbleImageDataForItemAtIndexPath:) 方法會要求我們為 CollectionView 中的每條消息數據提供一個與之相對應的 JSQMessageBubbleImageDataSource。這個方法正是我們設置氣泡圖片的好時機。
在 ChatViewController 添加方法:
override func collectionView(collectionView: JSQMessagesCollectionView!,
messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
逐行分析上面的代碼:
根據 NSIndexPath 檢索出對應的消息數據。
判斷這條消息是否是本客戶端所發出的,如果是,返回“發出消息”的 Image View。
如果不是,則返回“接收消息”的 ImageView。
在運行程序之前的最后一個步驟,是刪除頭像以及頭像刪除后留下的空白。
在 ChatViewController 中加入方法:
override func collectionView(collectionView: JSQMessagesCollectionView!,
avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
然后,在 viewDidLoad() 加入代碼:
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSizeZero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero
JSQMessagesViewController 支持頭像顯示,但我們用不到(或者不想),因為我們的 App 是一個匿名的聊天室。要刪除頭像顯示,只需要在詢問每條消息的頭像時返回一個 nil 并將頭像的 size 指定為 CGSizeZero,即“大小為 0”。
接下來開始對話并發送幾條消息!
發送消息
在 ChatViewController 中增加方法:
func addMessage(id: String, text: String) {
let message = JSQMessage(senderId: id, displayName: "", text: text)
messages.append(message)
}
這個工具方法用于創建一條新的 displayName 為空的 JSQMessage,然后將它添加到數據源中。
在 viewDidAppear() 方法中硬編碼幾條消息以便我們能真正看到它們:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// messages from someone else
addMessage("foo", text: "Hey person!")
// messages sent from local sender
addMessage(senderId, text: "Yo!")
addMessage(senderId, text: "I like turtles!")
// animates the receiving of a new message on the view
finishReceivingMessage()
}
運行程序,你會看到會話窗口中顯示了幾條聊天消息:
呃,接收消息中的文字也太不顯眼了。最好將它設置成黑色。
消息氣泡中的文字
正如你所見,JSQMessagesViewController 中幾乎每樣東西都和一個委托方法有關。要設置文字顏色,我們可以使用經典的 collectionView(_:cellForItemAtIndexPath:) 方法。
在 ChatViewController 中加入一個方法:
override func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath)
as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId {
cell.textView.textColor = UIColor.whiteColor()
} else {
cell.textView.textColor = UIColor.blackColor()
}
return cell
}
如果消息是本客戶端用戶所發,則文字顏色為白色,否則文字顏色為黑色。
運行程序,接收消息的文字變成黑色的了:
哇——看起來養眼多了!是時候讓它真正使用 Firebase 了。
Firebase 數據結構
在開始讓數據實時同步之前,先花點時間來看看數據結構。
Firebase 數據庫是 NoSQL 數據庫,也就是說,在 Firebase 數據庫中的每個對象都是 JSON 對象,這個 JSON 對象的每一個 key 都可以通過不同的 URL 來訪問。
舉一個例子,你的數據很可能是由這樣一個 JSON 構成:
{
// https://<my-firebase-app>.firebaseio.com/messages
"messages": {
"1": { // https://<my-firebase-app>.firebaseio.com/messages/1
"text": "Hey person!", // https://<my-firebase-app>.firebaseio.com/messages/1/text
"senderId": "foo" // https://<my-firebase-app>.firebaseio.com/messages/1/senderId
},
"2": {
"text": "Yo!",
"senderId": "bar"
},
"2": {
"text": "Yo!",
"senderId": "bar"
},
}
}
Firebase 數據庫支持“不規范的”數據結構,因此在每個 message 中都包含 senderId 是可以的。“不規范”的數據結構會導致一些數據冗余,但優點是檢索數據的速度更快。權衡下來——我們還是可以接受的。
創建 Firebase 引用
在 ChatViewController.swift 中增加屬性:
let rootRef = FIRDatabase.database().referenceFromURL("https://<my-firebase-app>.firebaseio.com/messages/")
var messageRef:FIRDatabaseReference!
譯者注:將 \ 替換成你自己的 Firebase App ID。
在 viewDidLoad() 方法中,初始化 messageRef:
override func viewDidLoad() {
super.viewDidLoad()
title = "ChatChat"
setupBubbles()
collectionView.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
messageRef = rootRef.child("messages")
}
我們創建了一個 rootRef 對象用于連接 Firebase 數據庫。然后用 child() 方法創建了一個 messageRef 對象,這個方法可用于創建下級引用。
不要奇怪,創建另一個引用并不意味著就需要創建新的連接。所有的引用其實可以共享同一個 Firebase 數據庫連接。
發送消息
你可能迫不及待地想點擊“Send”按鈕了,如果你這樣做,你會讓 App 崩潰。現在,你已經連上了 Firebase 數據庫,你可以真正地去發送幾條消息了。
首先,刪除 ChatViewController 中 viewDidAppear(_:) 方法中的測試消息。
然后,覆蓋下面的這個方法。這個方法允許發送按鈕將一條消息保存到 Firebase 數據庫:
//發送消息
override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: NSDate!) {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"text":text,
"senderId":senderId
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
}
這個方法負責:
1、通過 childByAutoId(),我們獲得一個子對象引用,該對象有一個自動創建的唯一 key。
2、用一個字典來保存消息。一個 [String:AnyObject] 足以表示一個 JSON 對象。
3、將字典保存到新的子引用中。
4、播放一個經典的代表“消息已發送”的聲音。
5、完成“發送”動作,將輸入欄置空。
運行程序,打開你的 Firebase App Dashboard,點擊 Data 欄。在 App 中發送一條消息,你立即會在 Dashboard 中看到這條消息顯示。
茄子注:抱歉,實在不想重制gif,麻煩得要死ORZ。
成功了!你已經很“專業”地將消息保存到了 Firebase 數據庫。這個消息還沒有顯示到 iPhone 上,但我們接下來就會這樣做。
與 Firebase 保持實時同步
每當我們改變 Firebase 數據庫中的數據,數據庫就會將修改 push 給每個已經連接的 App 上。Firebase 的數據同步機制分為三個部分:URL、事件和快照。
例如,你可以用這種方式來監聽新的消息:
let rootRef = FIRDatabase.database().referenceFromURL("https://fir-demo-43879.firebaseio.com/")
rootRef.observeEventType(.ChildChanged) { (snapshot:FIRDataSnapshot) in
print(snapshot.value)
}
這段代碼主要是:
通過 Firebase App URL,我們創建了一個 Firebase 數據庫引用。我們指定的這個 URL 指向了我們想讀取的數據。
用 FEventType.ChildAdded 參數調用 observeEventType(_:FEventType:) 方法。在每當位于該 URL 的對象添加了新的子對象時都會觸發一次 child-added 事件。
閉包中會傳入一個 FDataSnapshot 對象,這個對象中會包含有相應的數據以及一些有用的方法。
同步數據源
看到了吧,和 Firebase 保持數據同步是非常簡單的,接下來是和數據源進行對接。
在 ChatViewController 中增加一個方法:
//監聽消息
func observeMessages() {
//1
let messagesQuery = messageRef.queryLimitedToLast(25)
//2
messagesQuery.observeEventType(.ChildAdded) { [weak self] (snpaShot:FIRDataSnapshot) in
//3
guard let dict = snpaShot.value as? [String:AnyObject] else { return }
guard let id = dict["senderId"] as? String else { return }
guard let text = dict["text"] as? String else { return }
//4
self?.addMessage(id, text: text)
//5
self?.finishReceivingMessage()
}
}
這個方法主要是:
1、創建一個查詢,限制要同步的數據為 25 條記錄。
2、監聽指定位置上的 .ChildAdded 事件,當結果集中有新的子對象添加和即將添加時觸發此事件。
3、從 snapshot.value 上讀取 senderId 和 text。
4、調用 addMessage() 方法將新消息添加到數據源。
5、通知 JSQMessagesViewControllers(),收到一條消息。
然后,在 viewDidAppear(_:) 方法中調用 observeMessages() 方法:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
observeMessages()
}
運行程序,你會看到新發送的消息會附加到已經發送的消息之后:
恭喜你!你已經有了一個實時聊天 App!現在該做一些更神奇的事了,比如說提示用戶正在輸入。
當用戶正在輸入時進行提示
這個 App 最酷的特性之一是提示“用戶正在輸入…”信息。會有一個小氣泡彈出,告訴你用戶正在鍵盤上敲擊。這個提示非常重要,因為它讓我們減少了許多諸如“還在嗎?”之類的不必要的消息。
有許多方法可以檢測是否正在輸入,但 textViewDidChange(_:textView:) 方法是最好的方法。例如:
override func textViewDidChange(textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
print(textView.text != "")
}
要判斷用戶是否正在敲擊鍵盤,只需要檢查 textView.text 的值。如果這個值不為空,我們就可以認為用戶正在輸入。
通過 Firebase,我們可以在用戶輸入時向 Firebase 數據庫更新狀態。然后,通過從數據庫檢索這個狀態,顯示“用戶正在輸入”的提示。
首先在 ChatViewController 中增加幾個屬性:
var userIsTypingRef:FIRDatabaseReference! //1
private var localTyping = false //2
var isTyping: Bool {
set{
//3
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
get{
return localTyping
}
}
這些屬性分別用于:
1、一個 FIRDatabaseReference 引用,用于存儲當前用戶是否正在輸入。
2、一個私有屬性,用于記錄當前用戶是否正在輸入。
3、一個計算屬性,通過簡單地給這個屬性賦值,就可以實時修改 userIsTypingRef。
在 ChatViewController 添加一個方法:
//監聽輸入
func observeIsTyping() {
let typingIndicatorRef = rootRef.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
}
這個方法創建了一個引用,指向 URL “/typingIndicator”,這個地址用于更新用戶的輸入狀態。當用戶退出后,我們不需要這個數據了,因此我們可以用 onDiscounnectRemoveValue() 指定,當用戶離開則刪除該數據。
在 viewDidAppear(_:) 方法中調用 observeTyping() 方法:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
observeMessages()
observeTyping()
}
在 ChatViewController 中添加 textViewDidChange(_:textView:) 方法,修改 isTyping 的值:
override func textViewDidChange(textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
isTyping = textView.text != ""
}
最后,在 didPressSendButton(_:withMessageText:senderId:senderDisplayName:date:) 方法最后重置輸入提示:
isTyping = false
運行程序,在 Firebase App Dashboard 中觀察數據。當你輸入消息內容時,你會看到這個用戶的 typingIndicator 會隨之改變:
噢!現在你已經能夠知道用戶什么時候輸入了!讓我們來顯示這個提示。
查詢哪些用戶正在輸入
“用戶正在輸入”應當在用戶輸入的時候顯示,但不應當計算本地用戶。我們沒有必要知道(我們已經知道)當前本地用戶是否正在輸入。
用一個 Firebase 查詢,我們可以知道當前正在輸入的所有用戶。在 ChatViewController 中加入一個屬性:
var userTypingQuery:FIRDatabaseQuery!
然后,修改 observeTyping() 為:
//監聽輸入
func observeIsTyping() {
let typingIndicatorRef = rootRef.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
//1
userTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqualToValue(true)
//2
userTypingQuery.observeEventType(.Value) { [weak self] (snapShot:FIRDataSnapshot) in
if let weakself = self {
//3 You're the only typing, don't show the indicator
if snapShot.childrenCount == 1 && weakself.isTyping { return }
// 4 Are there others typing?
weakself.showTypingIndicator = snapShot.childrenCount > 1
weakself.scrollToBottomAnimated(true)
}
}
}
在代碼中,我們:
1、初始化一個查詢,用于查詢當前正在輸入的用戶。這一句相當于“喂,Firebase,去 /typingIndicator (這是一個對象,包含了若干鍵值對)下面看看,告訴我哪些鍵值對的值是 true。”
2、用 .Value 監聽改變,一旦這些值發生任何變化,就會立即通知你。
3、檢查結果中有多少用戶正在輸入。如果只有一個,則再檢查這個用戶是不是本地用戶,如果是,不顯示提示。
4、如果有不止一個用戶,而且本地用戶并沒有在輸入,則需要顯示輸入提示。最后,調用 scrollToBottomAnimated(_:animated:) 方法,確保輸入提示能夠被看到。
在運行程序之前,還需要一臺物理設備,因為這個測試需要兩臺設備。用模擬器扮演一個用戶,而物理設備扮演另外一個用戶。
在這兩臺設備上(一臺是模擬器,一臺是物理設備)運行程序,當一個用戶在輸入時,你可以看到提示顯示了(注意氣泡中有省略號):
哇!你創建了一個偉大的、酷炫的、實時的、帶用戶輸入提示的聊天 App。人生如此,當浮一大白!
在這個教程中,你學會了如何使用 Firebase 和 JSQMessagesViewController,但仍然還有許多事情可做,比如 1 對 1 聊天,社交賬號登錄以及頭像顯示。
來自:http://www.jianshu.com/p/98eb3356593b