原文
原文是基于Firebase 2.X構(gòu)筑的匿名聊天室Demo,我在根據(jù)原文構(gòu)筑時FireBase已經(jīng)更新到3.X,谷歌對Firebase SDK做了不小的改動,所有的功能都由不同的類來操作,不再由Firebase類統(tǒng)一調(diào)度,這些改動致使原文某些地方變得不合時宜。
因此,我將自己根據(jù)原文構(gòu)筑的基于3.X Firebase的流程及所遇到的坑穿插在原文中并且重新構(gòu)筑了原文的部分代碼。(有沒有覺得構(gòu)筑這兩個字很有逼格?!)
這篇文章是基于Firebase 3.X的簡單教程,若是不需要的就右上角吧(=?ω?)=。
現(xiàn)在主流的 App 都開始支持聊天功能了——你的 App 是不是也該支持一下?
但是,制作一個聊天工具確實不是一件簡單的任務(wù)。我們不但缺乏現(xiàn)成的專門針對聊天的 UIKit 組件,還需要一個服務(wù)器來負責處理用戶間的消息及對話。
幸運的是,我們可以使用一個優(yōu)秀的框架:Firebase。它能為我們同步實時數(shù)據(jù)而無需編寫任何服務(wù)端代碼,同時還提供一個 JSQMessagesViewController 用于顯示消息,這個 UI 可以和本地的消息應(yīng)用相媲美。
在這個 Firebase 教程中,我們會創(chuàng)建一個可以進行匿名聊天的 App 叫做 ChatChat,如下圖所示:
最終你將學習到:
1、用 CocoaPods 安裝 Firebase SDK 和 JSQMessagesViewController。
2、用 Firebase 數(shù)據(jù)庫來同步實時數(shù)據(jù)。
3、讓 Firebase 支持匿名登錄。
4、用 JSQMessagesViewController 實現(xiàn) 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 對于國內(nèi)用戶來說并不友好,經(jīng)常出現(xiàn)各種無法 pod install 的情況。如果是這樣,你必須手動安裝這兩個庫了。關(guān)于 Firebase 的手動安裝,請看這里。關(guān)于 JSQMessagesViewController 的手動安裝,你需要從 github 下載 JSQMessagesViewController 和 JSQSystemSoundPlayer 這兩個庫的源文件然后添加到項目里,并改正 JSQSystemSoundPlayer+JSQMessages.m 中的兩個錯誤即可,然后在橋接頭文件中導入相應(yīng)的 .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時慘遭谷歌翻臉,然后在手動集成時又被官方文檔坑了,在此將手動集成時的一些注意點根據(jù)自己手動集成的流程說明。
首先,你可以在這里下載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中,如下圖:
再次運行你就能看到正常的運行畫面:
注意:在接下來的教程中,你每次編譯和運行都會看到這個界面。點擊“匿名登錄”又會切換到另一個界面。目前點擊按鈕沒有什么用處,但隨后我們就會實現(xiàn)它。
如果你第一次接觸 Firebase,你需要創(chuàng)建一個賬號。不用擔心—— 它非常簡單,而且完全是免費的,不需要信用卡。
注意:關(guān)于如何注冊 Firebase 的完整步驟,請看我們的Firebase 入門教程。
注冊 Firebase 賬號
進入 Firebase 注冊頁面,創(chuàng)建一個賬號,然后創(chuàng)建一個 Firebase App。就本教程而言,你需要使用實時數(shù)據(jù)庫和身份認證服務(wù)。
開啟匿名認證
Firebase 允許用戶通過 email 地址或社交賬號進行登錄,但也提供匿名登錄功能,后者會給每個用戶分配一個唯一的 ID 但不需要用戶的輸入任何個人信息。
匿名認證就好比說:“我不知道你是誰,我只知道你是一個人。”。對于訪問賬戶或者使用用戶來說,這是非常方便的。這對于本教程來說非常適合,因為 ChatChat 中所有用戶都是匿名的。
要開啟匿名認證,你需要進入你的Firebase 項目,選擇 Auth 標簽,若是初次創(chuàng)建則會指引你選擇登陸方法,啟用匿名登陸即可。
這樣,你就開啟了超級隱身模式,也就是匿名認證——很爽吧:]
登陸
打開 LoginViewController.Swift,加入:
import Firebase
要登入聊天室,需要連接到 Firebase 數(shù)據(jù)庫。在 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 創(chuàng)建一個 Firebase 數(shù)據(jù)庫連接并賦給這個屬性。
如果你不知道你的 Firebase App URL 是什么,就像上一步一樣點擊Database即可 :
茄子注,雖然Firebase沒有向我們提供Database的源代碼,但從其API上大致可以猜測一二,使用URL進行首次連接,接著在注銷之前便會無限制地互發(fā)數(shù)據(jù),這點上與WebSocket的機制非常相似,我們有理由猜測FireDatabase的底層是利用WebSocket的概念實現(xiàn)的。
要登錄一個用戶,可以在數(shù)據(jù)庫引用對象上調(diào)用 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、調(diào)用 FiRAuth的單例 的 authAnonyouslyWithCompletionBlock(_:) 方法以匿名方式登錄一個用戶。
2、檢查是否認證失敗。
3、在閉包中,調(diào)用 segue 跳轉(zhuǎn)到 ChatViewController。
閉包回調(diào)中的user包含了用戶的唯一標示符及是否匿名登陸等重要信息,當我們在使用到時可以通過:
FIRAuth.auth()?.currentUser
來獲得相應(yīng)信息。
茄子注:匿名登陸并不會隨時改變,就像一般的賬戶一樣通過一個refreshToken來實現(xiàn)長期登陸,這個屬性被加密成無規(guī)則字符串。
創(chuàng)建聊天界面
JSQMessagesViewController 是一個 UICollectionViewController 的封裝,為聊天進行了專門的定制。
本教程將主要介紹 5 個步驟:
1、創(chuàng)建消息數(shù)據(jù)
2、創(chuàng)建帶背景色的消息氣泡
3、刪除頭像
4、改變 UICollectionViewCell 的文字顏色
5、提示用戶正在輸入
幾乎每個步驟都需要覆蓋一些方法。JSQMessagesViewController 使用了JSQMessagesCollectionViewDataSource 協(xié)議,因此我們需要覆蓋協(xié)議的默認實現(xiàn)就可以了。
注意:關(guān)于 JSQMessagesCollectionViewDataSource 的更多內(nèi)容,請參考這里。
打開 ChatViewController.swift 導入 Firebase 和JSQMVC :
import Firebase
import JSQMessagesViewController
將父類從 UIViewController 類修改 JSQMessagesViewController 類:
class ChatViewController: JSQMessagesViewController {
現(xiàn)在 ChatViewController 繼承了 JSQMessagesViewController,我們可以設(shè)置 senderId 和 senderDisplayName 的初始值了,這樣 App 才能唯一識別消息的發(fā)送者——否則它無從知道發(fā)送者是誰。
在 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 并轉(zhuǎn)換成 UINavigationController。
2、將 Navigation Controller 的第一個 View Controller 轉(zhuǎn)換成ChatVC。
3、將本地用戶的 ID 賦給 chatVc.senderId,這是 JSQMVC 用于處理消息的客戶端 ID。
4、將 chatVc.senderDisplayName 設(shè)為空字符串,因為我們的聊天室是匿名登錄的。
注意每個 App 會話中,我們只會有一個匿名的會話。每次重啟 App 之后,你都會獲得一個新的、唯一的匿名用戶。如果你重啟模擬器,你會看到另外一個用戶 ID。
茄子注:上面那段話存在于原譯文之中,我沒有與英文原文核實是否英文原文存在。有可能是Firebase的機制變化了,我們可以從不斷重啟模擬器、打印user.uid及通過Firebase項目的Auth頁面查看驗證得到user.uid沒有發(fā)生變化,因此每次重啟App之后都會得到一個新的匿名用戶是不正確的。
運行程序,檢查你的 App 是否運行在超級隱身模式:
通過簡單地繼承下 JSQMessagesViewController,你就獲得了一個完整的聊天 UI。太爽了!
創(chuàng)建數(shù)據(jù)源及委托
現(xiàn)在,我們有了一個聊天 UI,你可能很想在上面顯示點什么了。但首先,你需要注意幾件事情。
要顯示聊天消息,我們需要一個數(shù)據(jù)源,即一個實現(xiàn)了 JSMessageData 協(xié)議的對象并實現(xiàn)一些委托方法。我們可以自己定義一個類來實現(xiàn) JSQMessageData 協(xié)議,也可以使用現(xiàn)成的 JSQMessage 類。
在 ChatViewController 頭部,定義一個屬性:
// MARK: Properties
var messages = [JSQMessage]()
messages 屬性是一個數(shù)組,存儲了多個 JSQMessage 實例。
在 ChatViewController 中,實現(xiàn) 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:) 方法完全一樣。
還有幾個必須實現(xiàn)的委托方法,用于提供消息數(shù)據(jù)、氣泡圖片以及頭像。提供消息數(shù)據(jù)的方法已經(jīng)實現(xiàn)了,接下來就是提供氣泡和頭像的方法。
氣泡顏色
在 Collection View 中,消息文本顯示在一個簡單的圖片背景之上。有兩種類型的消息:收到的消息和發(fā)出的消息。發(fā)出的消息靠右側(cè)顯示而收到的消息則靠左顯示。
在 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 有創(chuàng)建聊天氣泡的方法。在 JSQMessagesViewController 中有一個 Category,允許我們使用原生消息 App 中消息氣泡所使用的顏色。
通過 bubbleImageFactory.outgoingMessagesBubbleImageWithColor() 和 bubbleImageFactory.incomingMessagesBubbleImageWithColor() 方法,我們可以創(chuàng)建出接收消息和發(fā)出消息的氣泡圖片。
然后,在 viewDidLoad() 方法中調(diào)用這個 setupBubbles() 方法:
override func viewDidLoad() {
super.viewDidLoad()
title = "ChatChat"
setupBubbles()
}
設(shè)置氣泡圖片
要為每條消息設(shè)置顏色氣泡,我們需要覆蓋 JSQMessagesCollectionViewDataSource 協(xié)議中的一個方法。
collectionView(_:messageBubbleImageDataForItemAtIndexPath:) 方法會要求我們?yōu)?CollectionView 中的每條消息數(shù)據(jù)提供一個與之相對應(yīng)的 JSQMessageBubbleImageDataSource。這個方法正是我們設(shè)置氣泡圖片的好時機。
在 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
}
}
逐行分析上面的代碼:
根據(jù) NSIndexPath 檢索出對應(yīng)的消息數(shù)據(jù)。
判斷這條消息是否是本客戶端所發(fā)出的,如果是,返回“發(fā)出消息”的 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”。
接下來開始對話并發(fā)送幾條消息!
發(fā)送消息
在 ChatViewController 中增加方法:
func addMessage(id: String, text: String) {
let message = JSQMessage(senderId: id, displayName: "", text: text)
messages.append(message)
}
這個工具方法用于創(chuàng)建一條新的 displayName 為空的 JSQMessage,然后將它添加到數(shù)據(jù)源中。
在 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()
}
運行程序,你會看到會話窗口中顯示了幾條聊天消息:
呃,接收消息中的文字也太不顯眼了。最好將它設(shè)置成黑色。
消息氣泡中的文字
正如你所見,JSQMessagesViewController 中幾乎每樣東西都和一個委托方法有關(guān)。要設(shè)置文字顏色,我們可以使用經(jīng)典的 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
}
如果消息是本客戶端用戶所發(fā),則文字顏色為白色,否則文字顏色為黑色。
運行程序,接收消息的文字變成黑色的了:
哇——看起來養(yǎng)眼多了!是時候讓它真正使用 Firebase 了。
Firebase 數(shù)據(jù)結(jié)構(gòu)
在開始讓數(shù)據(jù)實時同步之前,先花點時間來看看數(shù)據(jù)結(jié)構(gòu)。
Firebase 數(shù)據(jù)庫是 NoSQL 數(shù)據(jù)庫,也就是說,在 Firebase 數(shù)據(jù)庫中的每個對象都是 JSON 對象,這個 JSON 對象的每一個 key 都可以通過不同的 URL 來訪問。
舉一個例子,你的數(shù)據(jù)很可能是由這樣一個 JSON 構(gòu)成:
{
// 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 數(shù)據(jù)庫支持“不規(guī)范的”數(shù)據(jù)結(jié)構(gòu),因此在每個 message 中都包含 senderId 是可以的。“不規(guī)范”的數(shù)據(jù)結(jié)構(gòu)會導致一些數(shù)據(jù)冗余,但優(yōu)點是檢索數(shù)據(jù)的速度更快。權(quán)衡下來——我們還是可以接受的。
創(chuàng)建 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")
}
我們創(chuàng)建了一個 rootRef 對象用于連接 Firebase 數(shù)據(jù)庫。然后用 child() 方法創(chuàng)建了一個 messageRef 對象,這個方法可用于創(chuàng)建下級引用。
不要奇怪,創(chuàng)建另一個引用并不意味著就需要創(chuàng)建新的連接。所有的引用其實可以共享同一個 Firebase 數(shù)據(jù)庫連接。
發(fā)送消息
你可能迫不及待地想點擊“Send”按鈕了,如果你這樣做,你會讓 App 崩潰。現(xiàn)在,你已經(jīng)連上了 Firebase 數(shù)據(jù)庫,你可以真正地去發(fā)送幾條消息了。
首先,刪除 ChatViewController 中 viewDidAppear(_:) 方法中的測試消息。
然后,覆蓋下面的這個方法。這個方法允許發(fā)送按鈕將一條消息保存到 Firebase 數(shù)據(jù)庫:
//發(fā)送消息
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(),我們獲得一個子對象引用,該對象有一個自動創(chuàng)建的唯一 key。
2、用一個字典來保存消息。一個 [String:AnyObject] 足以表示一個 JSON 對象。
3、將字典保存到新的子引用中。
4、播放一個經(jīng)典的代表“消息已發(fā)送”的聲音。
5、完成“發(fā)送”動作,將輸入欄置空。
運行程序,打開你的 Firebase App Dashboard,點擊 Data 欄。在 App 中發(fā)送一條消息,你立即會在 Dashboard 中看到這條消息顯示。
茄子注:抱歉,實在不想重制gif,麻煩得要死ORZ。
成功了!你已經(jīng)很“專業(yè)”地將消息保存到了 Firebase 數(shù)據(jù)庫。這個消息還沒有顯示到 iPhone 上,但我們接下來就會這樣做。
與 Firebase 保持實時同步
每當我們改變 Firebase 數(shù)據(jù)庫中的數(shù)據(jù),數(shù)據(jù)庫就會將修改 push 給每個已經(jīng)連接的 App 上。Firebase 的數(shù)據(jù)同步機制分為三個部分:URL、事件和快照。
例如,你可以用這種方式來監(jiān)聽新的消息:
let rootRef = FIRDatabase.database().referenceFromURL("https://fir-demo-43879.firebaseio.com/")
rootRef.observeEventType(.ChildChanged) { (snapshot:FIRDataSnapshot) in
print(snapshot.value)
}
這段代碼主要是:
通過 Firebase App URL,我們創(chuàng)建了一個 Firebase 數(shù)據(jù)庫引用。我們指定的這個 URL 指向了我們想讀取的數(shù)據(jù)。
用 FEventType.ChildAdded 參數(shù)調(diào)用 observeEventType(_:FEventType:) 方法。在每當位于該 URL 的對象添加了新的子對象時都會觸發(fā)一次 child-added 事件。
閉包中會傳入一個 FDataSnapshot 對象,這個對象中會包含有相應(yīng)的數(shù)據(jù)以及一些有用的方法。
同步數(shù)據(jù)源
看到了吧,和 Firebase 保持數(shù)據(jù)同步是非常簡單的,接下來是和數(shù)據(jù)源進行對接。
在 ChatViewController 中增加一個方法:
//監(jiān)聽消息
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、創(chuàng)建一個查詢,限制要同步的數(shù)據(jù)為 25 條記錄。
2、監(jiān)聽指定位置上的 .ChildAdded 事件,當結(jié)果集中有新的子對象添加和即將添加時觸發(fā)此事件。
3、從 snapshot.value 上讀取 senderId 和 text。
4、調(diào)用 addMessage() 方法將新消息添加到數(shù)據(jù)源。
5、通知 JSQMessagesViewControllers(),收到一條消息。
然后,在 viewDidAppear(_:) 方法中調(diào)用 observeMessages() 方法:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
observeMessages()
}
運行程序,你會看到新發(fā)送的消息會附加到已經(jīng)發(fā)送的消息之后:
恭喜你!你已經(jīng)有了一個實時聊天 App!現(xiàn)在該做一些更神奇的事了,比如說提示用戶正在輸入。
當用戶正在輸入時進行提示
這個 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 數(shù)據(jù)庫更新狀態(tài)。然后,通過從數(shù)據(jù)庫檢索這個狀態(tài),顯示“用戶正在輸入”的提示。
首先在 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 添加一個方法:
//監(jiān)聽輸入
func observeIsTyping() {
let typingIndicatorRef = rootRef.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
}
這個方法創(chuàng)建了一個引用,指向 URL “/typingIndicator”,這個地址用于更新用戶的輸入狀態(tài)。當用戶退出后,我們不需要這個數(shù)據(jù)了,因此我們可以用 onDiscounnectRemoveValue() 指定,當用戶離開則刪除該數(shù)據(jù)。
在 viewDidAppear(_:) 方法中調(diào)用 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 中觀察數(shù)據(jù)。當你輸入消息內(nèi)容時,你會看到這個用戶的 typingIndicator 會隨之改變:
噢!現(xiàn)在你已經(jīng)能夠知道用戶什么時候輸入了!讓我們來顯示這個提示。
查詢哪些用戶正在輸入
“用戶正在輸入”應(yīng)當在用戶輸入的時候顯示,但不應(yīng)當計算本地用戶。我們沒有必要知道(我們已經(jīng)知道)當前本地用戶是否正在輸入。
用一個 Firebase 查詢,我們可以知道當前正在輸入的所有用戶。在 ChatViewController 中加入一個屬性:
var userTypingQuery:FIRDatabaseQuery!
然后,修改 observeTyping() 為:
//監(jiān)聽輸入
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、初始化一個查詢,用于查詢當前正在輸入的用戶。這一句相當于“喂,F(xiàn)irebase,去 /typingIndicator (這是一個對象,包含了若干鍵值對)下面看看,告訴我哪些鍵值對的值是 true。”
2、用 .Value 監(jiān)聽改變,一旦這些值發(fā)生任何變化,就會立即通知你。
3、檢查結(jié)果中有多少用戶正在輸入。如果只有一個,則再檢查這個用戶是不是本地用戶,如果是,不顯示提示。
4、如果有不止一個用戶,而且本地用戶并沒有在輸入,則需要顯示輸入提示。最后,調(diào)用 scrollToBottomAnimated(_:animated:) 方法,確保輸入提示能夠被看到。
在運行程序之前,還需要一臺物理設(shè)備,因為這個測試需要兩臺設(shè)備。用模擬器扮演一個用戶,而物理設(shè)備扮演另外一個用戶。
在這兩臺設(shè)備上(一臺是模擬器,一臺是物理設(shè)備)運行程序,當一個用戶在輸入時,你可以看到提示顯示了(注意氣泡中有省略號):
哇!你創(chuàng)建了一個偉大的、酷炫的、實時的、帶用戶輸入提示的聊天 App。人生如此,當浮一大白!
接下來做什么
本教程代碼可在此下載。
Firebase 3.X代碼在此下載
在這個教程中,你學會了如何使用 Firebase 和 JSQMessagesViewController,但仍然還有許多事情可做,比如 1 對 1 聊天,社交賬號登錄以及頭像顯示。
要進一步完善此 App,請參考 Firebase iOS 文檔。
如果想了解如何用社交賬號登錄用戶,請參考Firebase 用戶認證指南,其中包括了 Twitter、Google、Facebook 和 GitHub。
我希望你喜歡這篇教程,如果有任何問題,請在下面的評論中(非匿名的,而且支持頭像)提出。
茄子注:邊寫Demo邊寫博文,到最后才發(fā)現(xiàn)除了手動安裝的一些坑及Firebase 3.X API 的改動外沒太大變化,但也寫完了還是發(fā)出來吧。