Firebase 教程: iOS 實時聊天
原文:https://www.raywenderlich.com/140836/firebase-tutorial-real-time-chat-2
貌似市場上的主流 app 都有聊天功能,所以,我們的 app 也應當新增聊天功能啦。
然而,開發一個聊天工具是一個令人畏懼的工作。除了要有專門用於聊天的本地 UIKit 控制元件,我們還需要一個伺服器來協呼叫戶之間的訊息和對話。
幸運的是,有一些不錯的框架可以幫助我們:在Firebase 的幫助下,我們可以不用寫一行後端程式碼就可同步實時資料,而 JSQMessagesViewController 則給我們提供了一個與原生訊息 app 相似的訊息傳遞 UI 。
在這篇 Firebase 教程中,我們將開發一個 RIC (Really Instant Chat) – 匿名聊天應用。如果你使用過 IRC 或者 Slack,這種 app 你應該很熟悉了。
在此教程,您將學習到如下內容:
- 使用 CocoaPods 設定 Firebase SDK 和 JSQMessagesViewController。
- 使用 Firebase 資料庫實時同步資料。
- Firebase 匿名身份驗證。
- 使用 JSQMessagesViewController 做為完整的聊天介面。
- 指示使用者何時輸入。
- 使用 Firebase 儲存。
開始
下載初始工程 the starter project here 。現在,它包含一個簡單的虛擬登入介面。
我們使用 CocoaPods 下載 Firebase SDK 和 JSQMessagesViewController。如果你還不會使用 CocoaPods ,請先學習我們這篇教程 Cocoapods with Swift tutorial。
在專案目錄下,進入終端,開啟根目錄下的 Podfile 檔案,新增如下依賴程式碼:
pod 'Firebase/Storage'
pod 'Firebase/Auth'
pod 'Firebase/Database'
pod 'JSQMessagesViewController'
儲存檔案,命令列執行如下命令:
pod install
完成依賴包下載後,在 Xcode 開啟 ChatChat.xcworkspace 。在執行之前,先配置 Firebase 。
如果你從未使用過 Firebase,首先你需要建立一個賬號。不用擔心,這些是免費的。
注: Firebase 的操作細節,可以看這裡 Getting Started with Firebase tutorial.
建立 Firebase 賬號
登入 the Firebase signup site,建立賬號,然後建立一個工程。
按照指示將 Firebase 新增到 iOS 應用程式,複製 GoogleService-Info.plist 配置檔案到你的專案。它包含與應用程式的 Firebase 整合所需的配置資訊。
build and run ,你將看到如下介面:
允許匿名認證
Firebase允許使用者通過電子郵件或社交帳戶登入,但它也可以匿名地對使用者進行身份驗證,為使用者提供唯一的識別符號,而不需要了解他們任何資訊。
要設定匿名驗證,開啟 Firebase 應用程式的 Dashboard,選擇左側的 Auth 選項,單擊 “Sign-In” 方法,然後選擇“ Anonymous”,開啟 “ Enable” 按鈕,然後單擊 “Save”。
像這樣,我們啟用了超級祕密隱形模式 ! 好吧,雖然這只是匿名身份驗證,但它仍然很酷。
登入
開啟 LoginViewController.swift,新增 import UIKit:
import Firebase
要登入聊天,app 需要使用 Firebase 身份驗證服務進行身份驗證。將以下程式碼新增到loginDidTouch(_:):
if nameField?.text != "" { // 1
FIRAuth.auth()?.signInAnonymously(completion: { (user, error) in // 2
if let err = error { // 3
print(err.localizedDescription)
return
}
self.performSegue(withIdentifier: "LoginToChat", sender: nil) // 4
})
}
註釋如下:
1. 首先,確保 name field 非空。
2.使用 Firebase Auth API 匿名登入,該方法帶了一個方法塊兒,方法塊兒傳遞 user 和 error 資訊。
3. 在完成方法塊裡,檢查是否有認證錯誤,如果有,終止執行。
4. 最後,如果沒有錯誤異常,進入 ChannelListViewController 頁面。
Build and run,輸入你的名字,然後進入 app。
建立 Channels 列表
一旦使用者登入了, app 導航到 ChannelListViewController 頁面, 該頁面展示給使用者當前頻道列表, 給他們提供選擇建立新通道。該頁面使用兩個 section 的表檢視。第一個 section 提供了一個表單,使用者可以在其中建立一個新的通道,第二 section 列出所有已知通道。
本小節,我們將學到:
1. 儲存資料到 Firebase 資料庫
2. 監聽儲存到資料庫的新資料。
在 ChannelListViewController.swift 的頭部新增如下程式碼:
import Firebase
enum Section: Int {
case createNewChannelSection = 0
case currentChannelsSection
}
緊隨匯入語句之後的 enum 中包含兩個表檢視 section 。
接下來,在類內,新增如下程式碼:
// MARK: Properties
var senderDisplayName: String? // 1
var newChannelTextField: UITextField? // 2
private var channels: [Channel] = [] // 3
註釋如下 :
- 新增一個儲存 sender name 的屬性。
- 新增一個 text field ,稍後我們會使用它新增新的 Channels。
- 新增一個空的 Channel 物件陣列,儲存你的 channels。這是 starter 專案中提供的一個簡單的模型類,它只包含一個名稱和一個ID。
接下來,我們需要設定 UITableView 來呈現新的通道和可用的通道列表。在 ChannelListViewController.swift 中新增以下程式碼:
// MARK: UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return 2 // 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // 2
if let currentSection: Section = Section(rawValue: section) {
switch currentSection {
case .createNewChannelSection:
return 1
case .currentChannelsSection:
return channels.count
}
} else {
return 0
}
}
// 3
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue ? "NewChannel" : "ExistingChannel"
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
if (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue {
if let createNewChannelCell = cell as? CreateChannelCell {
newChannelTextField = createNewChannelCell.newChannelNameField
}
} else if (indexPath as NSIndexPath).section == Section.currentChannelsSection.rawValue {
cell.textLabel?.text = channels[(indexPath as NSIndexPath).row].name
}
return cell
}
對於以前使用過 UITableView 的人來說,這應該是非常熟悉的,但簡單地說幾點:
1. 設定 Sections。請記住,第一部分將包含一個用於新增新通道的表單,第二部分將顯示一個通道列表。
2. 為每個部分設定行數。第一部分設定為 1,第二部分設定個數為通道的個數。
3. 定義每個單元格的內容。對於第一個部分,我們將 cell 中的 text field 儲存在newChannelTextField 屬性中。對於第二部分,您只需將單元格的 text field 標籤設定為通道名稱。
為了確保這一切正常工作,請在屬性下面新增以下程式碼:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
channels.append(Channel(id: "1", name: "Channel1"))
channels.append(Channel(id: "2", name: "Channel2"))
channels.append(Channel(id: "3", name: "Channel3"))
self.tableView.reloadData()
}
這只是向通道陣列新增了一些虛擬通道。
Build and run app ; 再次登入,我們現在應該可以看到表單建立一個新的通道和三個虛擬通道:
太棒了! 接下來,我們需要讓它與 Firebase 一起工作了。 :]
Firebase 資料結構
在實現實時資料同步之前,首先讓我們花一會兒功夫想想資料結構。
Firebase database 以 NoSQL JSON 格式儲存資料。
基本上,Firebase資料庫中的所有內容都是JSON物件,而這個JSON物件的每個鍵都有自己的URL。
下面是一個說明我們的資料如何作為 JSON 物件的示例:
{
"channels": {
"name": "Channel 1"
"messages": {
"1": {
"text": "Hey person!",
"senderName": "Alice"
"senderId": "foo"
},
"2": {
"text": "Yo!",
"senderName": "Bob"
"senderId": "bar"
}
}
}
}
Firebase 資料庫支援非規範化的資料結構,因此可以為每個訊息項包含 senderId。一個非規範化的資料結構意味著我們將複製大量的資料,但好處是可以更快的檢索資料。
實時 Channel 同步
首先,刪除上面新增的viewDidAppear(_:)程式碼,然後在其他以下屬性中新增以下屬性:
private lazy var channelRef: FIRDatabaseReference = FIRDatabase.database().reference().child("channels")
private var channelRefHandle: FIRDatabaseHandle?
channelRef 將用於儲存對資料庫中通道列表的引用;channelRefHandle 將為引用儲存一個控制程式碼,以便以後可以刪除它。
接下來,我們需要查詢Firebase資料庫,並得到一個在我們的表檢視中顯示的通道列表。新增以下程式碼:
// MARK: Firebase related methods
private func observeChannels() {
// Use the observe method to listen for new
// channels being written to the Firebase DB
channelRefHandle = channelRef.observe(.childAdded, with: { (snapshot) -> Void in // 1
let channelData = snapshot.value as! Dictionary<String, AnyObject> // 2
let id = snapshot.key
if let name = channelData["name"] as! String!, name.characters.count > 0 { // 3
self.channels.append(Channel(id: id, name: name))
self.tableView.reloadData()
} else {
print("Error! Could not decode channel data")
}
})
}
程式碼解釋:
1. 我們在通道引用上呼叫 observe:with: 方法,將控制程式碼儲存到引用。每當在資料庫中新增新的通道時,就呼叫 completion block 。
2. completion 後接收到一個 FIRDataSnapshot (儲存在快照中),其中包含資料和其它有用的方法。
3. 我們將資料從快照中提取出來,如果成功,建立一個通道模型並將其新增到我們的通道陣列中。
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "RW RIC"
observeChannels()
}
deinit {
if let refHandle = channelRefHandle {
channelRef.removeObserver(withHandle: refHandle)
}
}
這將在 view controller 載入時呼叫新的 observeChannels() 方法。當 view controller 通過檢查 channelRefHandle 是否設定並呼叫 removeObserver(withHandle:) 來判斷是否結束生命週期時,我們同時停止觀察資料庫更改。
在看到從 Firebase 中提取出的通道列表之前,還有一件事需要做: 提供一種方法來建立通道! 在故事板中已經設定了 IBAction,所以只需向我們的類新增以下程式碼就好了:
// MARK :Actions
@IBAction func createChannel(_ sender: AnyObject) {
if let name = newChannelTextField?.text { // 1
let newChannelRef = channelRef.childByAutoId() // 2
let channelItem = [ // 3
"name": name
]
newChannelRef.setValue(channelItem) // 4
}
}
下面是詳細解釋:
1. 首先檢查 text field 是否擁有一個 channel name.
2. 使用 childByAutoId() 唯一標誌 key 建立一個通道引用。
3. 建立一個字典,以此儲存通道的資料。[String: AnyObject] 是類似 JSON 的物件。
4. 最後,在這個新的通道上設定名稱,它將自動儲存到Firebase !
Build and run 我們的 app ,建立一些 channels。
所有內容都應該按照預期執行,但我們還沒有實現當使用者點選時可以訪問其中一個通道。讓我們新增以下程式碼來解決這個問題:
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.section == Section.currentChannelsSection.rawValue {
let channel = channels[(indexPath as NSIndexPath).row]
self.performSegue(withIdentifier: "ShowChannel", sender: channel)
}
}
以上程式碼,我們應該很熟悉了。當使用者點選通道 cell 時,它會觸發 ShowChannel segue。
建立聊天介面
JSQMessagesViewController 是一個 UICollectionViewController 定製聊天控制類,所以我們不需要再建立自己的了!
這部分教程,我們將關注四點:
- 建立訊息資料。
- 建立訊息泡沫。
- 刪除頭像支援。
- 改變 UICollectionViewCell 的 文字顏色。
幾乎所有需要做的事情都需要覆蓋方法。JSQMessagesViewController 採用JSQMessagesCollectionViewDataSource 協議,所以我們只需要覆蓋預設的實現方法就好了。
注意:有關 JSQMessagesCollectionViewDataSource的更多資訊, 請檢視這裡的 Cocoa 文件。
開啟 ChatViewController.swift ,新增如下引入:
import Firebase
import JSQMessagesViewController
將繼承類 UIViewController 改為 JSQMessagesViewController:
final class ChatViewController: JSQMessagesViewController {
在 ChatViewController 頭部,定義如下屬性:
var channelRef: FIRDatabaseReference?
var channel: Channel? {
didSet {
title = channel?.name
}
}
既然 ChatViewController 繼承自JSQMessagesViewController , 我們需要設定 senderId 和 senderDisplayName 的初始值,以使 app 可以唯一標識訊息的傳送者——即使它不知道那個人具體是誰。
這些需要在 view controller 首次例項化時設定。最好的設定時刻是當 segue 即將 prepare 時。回到ChannelListViewController, 新增以下程式碼:
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let channel = sender as? Channel {
let chatVc = segue.destination as! ChatViewController
chatVc.senderDisplayName = senderDisplayName
chatVc.channel = channel
chatVc.channelRef = channelRef.child(channel.id)
}
}
這將在執行 segue 之前建立的 ChatViewController 上設定屬性。
獲得 senderDisplayName 的最佳位置是當使用者登入時輸入他們的名字。
在 LoginViewController.swift,新增如下方法:
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
let navVc = segue.destination as! UINavigationController // 1
let channelVc = navVc.viewControllers.first as! ChannelListViewController // 2
channelVc.senderDisplayName = nameField?.text // 3
}
註釋:
1. 從 segue 獲取目標檢視控制器並將其轉換為 UINavigationController。
2. 強制轉換 UINavigationController 的第一個view controller 為 ChannelListViewController。
3. 設定 ChannelListViewController 的senderDisplayName 為 nameField 中提供的使用者名稱。
返回 ChatViewController.swift,在 viewDidLoad() 方法最下方新增如下程式碼:
self.senderId = FIRAuth.auth()?.currentUser?.uid
這將基於已登入的 Firebase 使用者設定 senderId。
Build and run 我們的 app 並導航到一個 channel 頁面。
通過簡單地繼承 JSQMessagesViewController,我們得到一個完整的聊天介面。:]
設定 Data Source 和 Delegate
現在我們已經看到了新的很棒的聊天 UI,我們可能想要開始顯示訊息了。但在這麼做之前,我們必須注意一些事情。
要顯示訊息,我們需要一個資料來源來提供符合 JSQMessageData 協議的物件,我們還需要實現一些委託方法。雖然我們可以建立符合 JSQMessageData 協議的類,但我們將使用已經提供的 JSQMessage 類。
在 ChatViewController 頂部,新增如下屬性:
var messages = [JSQMessage]()
messages 是應用程式中儲存 JSQMessage 各種例項的陣列。
新增如下程式碼:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
對於上述兩種委託方法,我們並不陌生。第一個類似於 collectionView(_:cellForItemAtIndexPath:),只是管理的物件是 message data。第二種是在每個 section 中返回messages 數量的標準方法;
訊息氣泡顏色
在 collection view 中顯示的訊息只是文字覆蓋的影像。有兩種型別的訊息:傳出和傳入。傳出的訊息會顯示在右邊,傳入的訊息顯示在左邊。
在 ChatViewController 中新增如下程式碼:
private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
}
private func setupIncomingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
}
然後在頭部新增如下屬性:
lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
JSQMessagesBubbleImageFactory 有建立聊天泡泡的圖片方法,。JSQMessagesViewController 甚至還有一個類別提供建立訊息泡沫的顏色。
使用 outgoingMessagesBubbleImage (:with) 和incomingMessagesBubbleImage(: with)方法,我們可以建立輸入輸出影像。這樣,我們就有了建立傳出和傳入訊息氣泡所需的影像檢視了!
先別太興奮了,我們還需要實現訊息氣泡的委託方法。
設定氣泡影像
為每個 message 設定 colored bubble imag ,我們需要過載被collectionView(_:messageBubbleImageDataForItemAt:)呼叫的 JSQMessagesCollectionViewDataSource 方法。
這要求資料來源提供訊息氣泡影像資料,該資料對應於collectionView 中的 indexPath 中的 message 項。
在 ChatViewController 新增程式碼:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
以上程式碼註釋:
- 在這裡檢索訊息。
- 如果訊息是由本地使用者傳送的,則返回 outgoing image view。
- 相反,則返回 incoming image view.
移除頭像
JSQMessagesViewController 提供頭像,但是在匿名 RIC app 中我們不需要或者不想使用頭像。
在 ChatViewController 新增程式碼:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
為了移除 avatar image, 在每個 message’s avatar display 返回 nil 。
最後,在 viewDidLoad() 新增如下程式碼:
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
這將告訴佈局,當沒有 avatars 時,avatar 大小為 CGSize.zero。
檢查 app 構建,我們可以導航到我們的一個頻道;
是時候開始對話並新增一些資訊了!
建立訊息
在 ChatViewController 中建立如下方法:
private func addMessage(withId id: String, name: String, text: String) {
if let message = JSQMessage(senderId: id, displayName: name, text: text) {
messages.append(message)
}
}
該方法建立了一個 JSQMessage,並新增到 messages 資料來源中。
在 viewDidAppear(_:) 新增硬編碼訊息:
// messages from someone else
addMessage(withId: "foo", name: "Mr.Bolt", text: "I am so fast!")
// messages sent from local sender
addMessage(withId: senderId, name: "Me", text: "I bet I can run faster than you!")
addMessage(withId: senderId, name: "Me", text: "I like to run!")
// animates the receiving of a new message on the view
finishReceivingMessage()
Build and run,我們將看到如下效果:
恩,文字讀起來有點不爽,它應該顯示黑色的。
訊息氣泡文字
現在我們知道,如果想在 JSQMessagesViewController 做幾乎所有事情,我們只需要覆蓋一個方法。要設定文字顏色,請使用老式的collectionView(_:cellForItemAt:)。
在 ChatViewController 中新增如下方法:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId {
cell.textView?.textColor = UIColor.white
} else {
cell.textView?.textColor = UIColor.black
}
return cell
}
如果訊息是由本地使用者傳送的,設定文字顏色為白色。如果不是本地使用者傳送的,設定文字顏色為黑色。
這是一個很不錯的聊天 app! 是時候讓它與 Firebase 一起工作了。
Sending Messages
在 ChatViewController.swift 中新增如下屬性:
“`
private lazy var messageRef: FIRDatabaseReference = self.channelRef!.child(“messages”)
private var newMessageRefHandle: FIRDatabaseHandle?
這和我們在 ChannelListViewController 中新增的 channelRef、 channelRefHandle 屬性相似,我們應該很熟悉了。
接下來,刪除 ChatViewController 中的 viewDidAppear(_:) ,移除 stub test messages。
然後,重寫以下方法,使 "傳送" 按鈕將訊息儲存到 Firebase 資料庫。
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
let itemRef = messageRef.childByAutoId() // 1
let messageItem = [ // 2
“senderId”: senderId!,
“senderName”: senderDisplayName!,
“text”: text!,
]
itemRef.setValue(messageItem) // 3
JSQSystemSoundPlayer.jsq_playMessageSentSound() // 4
finishSendingMessage() // 5
}
註解:
1. 使用 childByAutoId(),建立一個帶有惟一鍵的子引用。
2. 然後建立一個字典來儲存訊息。
3. 接下來,儲存新子位置上的值。
4. 然後播放常規的 “訊息傳送” 聲音。
5. 最後,完成 "傳送" 操作並將輸入框重置為空。
Build and run; 開啟 Firebase 應用程式指示板並單擊 Data 選項卡。在應用程式中傳送一條訊息,我們就可以看到實時顯示在儀表板上的訊息了:
![Sending a message](http://upload-images.jianshu.io/upload_images/130752-6966090a573224fc.gif?imageMogr2/auto-orient/strip)
High five ! 我們已經可以像專業人員一樣將訊息儲存到 Firebase 資料庫了。現在訊息還不會出現在螢幕上,接下來我們將處理它。
##### 同步 Data Source
在 ChatViewController 中新增如下程式碼:
private func observeMessages() {
messageRef = channelRef!.child(“messages”)
// 1.
let messageQuery = messageRef.queryLimited(toLast:25)
// 2. We can use the observe method to listen for new
// messages being written to the Firebase DB
newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
// 3
let messageData = snapshot.value as! Dictionary
以下註解:
1. 首先建立一個查詢,將同步限制到最後 25 條訊息。
2. 使用 .ChildAdded 觀察已經新增到和即將新增到 messages 位置每個子 item。
3. 從 snapshot 中提取messageData。
4. 使用 addMessage(withId:name:text) 方法新增新訊息到資料來源。
5. 通知 JSQMessagesViewController,已經接收了訊息。
接下來,在 viewDidLoad() 中呼叫方法: observeMessages()。
Build and run,我們將看到我們前面輸入和現在輸入的所有訊息。
![Messages from firebase](http://upload-images.jianshu.io/upload_images/130752-402d03270957ef56.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
恭喜!我們已經有一個實時聊天應用了! 現在是做一些更高階的事情的時候了,比如在使用者輸入的時候檢測。
##### 檢測使用者何時在輸入
這款應用程式最酷的功能之一就是看到 "使用者正在輸入" 的指示器。當小氣泡彈出時,你知道另一個使用者在鍵盤上打字。這個指標非常重要,因為它可以避免我們傳送那些尷尬的 "你還在嗎?" 訊息。
檢測打字有很多方法,但 textViewDidChange(_:) 是一個很好的檢查時機。將以下內容新增到ChatViewController的底部:
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 中新增如下屬性:
```
private lazy var userIsTypingRef: FIRDatabaseReference =
self.channelRef!.child("typingIndicator").child(self.senderId) // 1
private var localTyping = false // 2
var isTyping: Bool {
get {
return localTyping
}
set {
// 3
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
}
以下是我們需要了解的這些特性:
- 建立一個用於跟蹤本地使用者是否正在輸入的 Firebase 引用。
- 新增私有屬性,標記本地使用者是否在輸入。
- 每次更改時,使用計算屬性更新 localTyping 和 userIsTypingRef。
現在,新增如下方法:
private func observeTyping() {
let typingIndicatorRef = channelRef!.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
}
這個方法建立一個名為 typingIndicator 的通道的子引用,它是我們更新使用者輸入狀態的地方。我們不希望這些資料在使用者登出之後仍然逗留,因此我們可以在使用者使用後刪除它 onDisconnectRemoveValue()。
新增以下內容呼叫新方法:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeTyping()
}
替換 textViewDidChange(_:) 中的 print(textView.text != “”) :
isTyping = textView.text != ""
這只是在使用者輸入時設定 isTyping。
最後,在 didPressSend(_:withMessageText:senderId:senderDisplayName:date:): 後面新增如下程式碼:
isTyping = false
當按下 Send 按鈕時,這將重置輸入指示器。
Build and run,開啟Firebase應用程式儀表板檢視資料。當我們鍵入訊息時,我們應該可以看到為使用者提供的型別指示器記錄更新:
我們現在已經知道什麼時候使用者在輸入了,接下來是顯示指示器的時候了。
查詢正在輸入的使用者
“使用者正在輸入” 指示符應該在除本地使用者外任何使用者鍵入時顯示,因為本地使用者在鍵入時自己已經知道啦。
使用 Firebase query ,我們可以檢索當前正在鍵入的所有使用者。在 ChatViewController 中新增如下屬性:
private lazy var usersTypingQuery: FIRDatabaseQuery =
self.channelRef!.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
這個屬性儲存了一個 FIRDatabaseQuery,它就像一個 Firebase 引用,但它是有序的。通過檢索所有正在輸入的使用者來初始化查詢。這基本上是說,“嘿,Firebase,查詢關鍵字 / typing 指示器,然後給我所有值為 true 的使用者。”
接下來,在 observeTyping() 新增如下程式碼:
// 1
usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
// 2 You're the only one typing, don't show the indicator
if data.childrenCount == 1 && self.isTyping {
return
}
// 3 Are there others typing?
self.showTypingIndicator = data.childrenCount > 0
self.scrollToBottom(animated: true)
}
註釋:
- 我們使用 .value 監聽狀態,當其值改變時,該 ompletion block 將被呼叫。
- 我們需要知道在查詢中有多少使用者,如果僅僅只有本地使用者,不顯示指示器。
- 如果有使用者,再設定指示器顯示。呼叫 scrolltobottom 動畫以確保顯示指示器。
在 build and run 之前,拿起一個物理 iOS 裝置,測試這種情況需要兩個裝置。一個使用者使用模擬器,另一個使用者使用真機。
現在,同時 build and run 模擬器和真機,當一個使用者輸入時,另外使用者可以看到指示器出現:
現在我們有了一個打字指示器,但我們還缺少一個現代通訊應用的一大特色功能——傳送圖片!
傳送圖片
要傳送影像,我們將遵循與傳送文字相同的原則,其中有一個關鍵區別: 我們將使用 Firebase 儲存,而不是直接將影像資料儲存在訊息中,這更適合儲存音訊、視訊或影像等大型檔案。
在 ChatViewController.swift 中新增 Photos :
import Photos
接下來,新增如下屬性:
lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "YOUR_URL_HERE")
這是一個 Firebase 儲存引用,概念上類似於我們已經看到的 Firebase 資料庫引用,但是對於儲存物件來說,用你的 Firebase 應用程式 URL 替換YOUR_URL_HERE,我們可以在你的應用程式控制臺中點選儲存。
傳送照片資訊需要一點點的 smoke 和 mirrors ,而不是在這段時間阻塞使用者介面,這會讓你的應用感覺很慢。儲存照片到Firebase 儲存返回一個URL,這可能需要幾秒鐘——如果網路連線很差的話,可能需要更長的時間。我們會用一個假的URL傳送照片資訊,並在照片儲存後更新訊息。
新增如下屬性:
private let imageURLNotSetKey = "NOTSET"
並新增方法:
func sendPhotoMessage() -> String? {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"photoURL": imageURLNotSetKey,
"senderId": senderId!,
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
return itemRef.key
}
這很像我們之前實現的 didPressSend(_:withMessageText:senderId:senderDisplayName:date:) 方法。
現在,我們需要能夠在獲取映像的 Firebase 儲存 URL之後更新訊息。新增以下:
func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["photoURL": url])
}
接下來,我們需要允許使用者選擇要傳送的影像。幸運的是 JSQMessagesViewController 已經包含新增一個影像到我們訊息的 UI ,所以我們只需要實現對應的方法處理點選就好了:
override func didPressAccessoryButton(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
picker.sourceType = UIImagePickerControllerSourceType.camera
} else {
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}
present(picker, animated: true, completion:nil)
}
這裡,如果裝置支援拍照,將彈出攝像機,如果不支援,會彈出相簿。
接下來,當使用者選擇影像,我們需要實現 UIImagePickerControllerDelegate方法來處理。將以下內容新增到檔案的底部(在最後一個關閉括號之後):
// MARK: Image Picker Delegate
extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
// 1
if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
// Handle picking a Photo from the Photo Library
// 2
let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
let asset = assets.firstObject
// 3
if let key = sendPhotoMessage() {
// 4
asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
let imageFileURL = contentEditingInput?.fullSizeImageURL
// 5
let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"
// 6
self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error.localizedDescription)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
})
}
} else {
// Handle picking a Photo from the Camera - TODO
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
}
註解:
1. 首先,從 info dictionary 獲取影像。
2. 呼叫 sendPhotoMessage() 方法,儲存影像 URL 到 Firebase 資料庫。
3. 接下來,我們將得到照片的 JPEG 表示,準備傳送到 Firebase 儲存。
4. 如前所述,根據使用者的惟一 id 和當前時間建立一個獨特的 URL。
5. 建立一個 FIRStorageMetadata 物件並將後設資料設定為 image / jpeg。
6. 然後儲存影像到 Firebase 資料庫。
7. 影像被儲存後,我們將再次呼叫 setImageURL() 方法。
幾近完美! 現在我們已經建立了可以將影像資料儲存到 Firebase 並將 URL 儲存到訊息資料儲存中的應用程式,但我們還沒有更新應用程式來顯示這些照片。接下來我們來解決這個問題。
展示影像
首先,在 ChatViewController 中新增屬性:
private var photoMessageMap = [String: JSQPhotoMediaItem]()
它包含一個 jsqphotomediaitem 陣列。
現在,我們需要為 addMessage (withId:name:text:) 建立一個兄弟方法。新增以下程式碼:
private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
messages.append(message)
if (mediaItem.image == nil) {
photoMessageMap[key] = mediaItem
}
collectionView.reloadData()
}
}
在這裡,如果影像鍵尚未設定,則將 JSQPhotoMediaItem 儲存在新屬性中。這允許我們在稍後設定影像時檢索並更新訊息。
我們還需要能夠從 Firebase 資料庫獲取影像資料,以便在UI中顯示它。新增以下方法:
private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
// 1
let storageRef = FIRStorage.storage().reference(forURL: photoURL)
// 2
storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading image data: \(error)")
return
}
// 3
storageRef.metadata(completion: { (metadata, metadataErr) in
if let error = metadataErr {
print("Error downloading metadata: \(error)")
return
}
// 4
if (metadata?.contentType == "image/gif") {
mediaItem.image = UIImage.gifWithData(data!)
} else {
mediaItem.image = UIImage.init(data: data!)
}
self.collectionView.reloadData()
// 5
guard key != nil else {
return
}
self.photoMessageMap.removeValue(forKey: key!)
})
}
}
註解:
1. 獲取儲存映像的引用。
2. 從儲存中獲取物件。
3. 從儲存中獲取影像後設資料。
4. 如果後設資料顯示影像是 GIF,我們需要使用 UIImage 類別,它通過 SwiftGifOrigin Cocapod 被拉進來。這是需要的,因為 UIImage 不處理 GIF 影像。否則我們只需要用普通的 UIImage 就可以了。
5. 最後,我們從 photoMessageMap 中刪除鍵,現在我們已經獲取了影像資料。
最後,我們需要更新 observeMessages()。在 if 語句中,但在 else 條件之前,新增以下測試:
else if let id = messageData["senderId"] as String!,
let photoURL = messageData["photoURL"] as String! { // 1
// 2
if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
// 3
self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
// 4
if photoURL.hasPrefix("gs://") {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
}
}
}
讓我們逐行解釋:
- 首先,檢查你是否有一個photoURL集。
- 如果可以,建立一個新的 JSQPhotoMediaItem。這個物件封裝了訊息中的富媒體——正是你所需要的!
- 呼叫 addPhotoMessage 方法。
- 最後,檢查一下,確保 photoURL 包含一個 Firebase 儲存物件的字首。如果是,獲取影像資料。
現在只剩下最後一件事了,你能猜到是什麼麼?
當你在解碼照片資訊時,你只是在你第一次觀察影像資料時才這樣做。但是,你還需要觀察稍後發生的訊息的任何更新,比如在將影像 URL 儲存到儲存後更新它。
新增下面屬性:
private var updatedMessageRefHandle: FIRDatabaseHandle?
在 observeMessages() 底部新增如下程式碼:
// We can also use the observer method to listen for
// changes to existing messages.
// We use this to be notified when a photo has been stored
// to the Firebase Storage, so we can update the message data
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String> // 1
if let photoURL = messageData["photoURL"] as String! { // 2
// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] { // 3
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key) // 4
}
}
})
註解:
- 從 Firebase 快照中獲取訊息資料字典。
- 檢查字典是否有一個 photoURL 鍵集。
- 如果是這樣,則從快取中提取 JSQPhotoMediaItem。
- 最後,獲取影像資料並使用影像更新訊息!
當 ChatViewController 消失時,我們需要做的最後一件事就是整理和清理。新增以下方法:
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}
Build and run 應用程式; 我們就應該能夠在聊天中點選小的 paperclip 圖示傳送照片或圖片資訊了。注意這些訊息何時顯示一個等待的小 spinner—— 當我們的應用程式儲存照片資料到 Firebase 儲存的時候。
Kaboom! 我們剛剛做了一個說大也大說小也小、實時的、使用者可以輸入照片和 GIF 的聊天應用程式。
Where to Go From Here?
Demo 下載地址: completed project
我們現在知道 Firebase 和 JSQMessagesViewController 的基本知識,但還有很多你可以做,包括 one-to-one messaging、social authentication、頭像顯示等。
想更多瞭解,請查閱 Firebase iOS documentation.
– 2017.10.24
上海 虹橋V1
相關文章
- Firebase Tutorial: Getting Started 教程翻譯
- [教程] 實現視訊對話應用 HouseParty教程(三)—— 多人聊天|附 iOS 原始碼iOS原始碼
- [教程] 實現視訊對話應用 HouseParty教程(二)—— 開始聊天|附 iOS 原始碼iOS原始碼
- Android Firebase接入(序)--Firebase簡介以及Firebase官方Demo的使用Android
- 小程式實現實時聊天IM功能
- Websocket 直播間聊天室教程 - GoEasy 快速實現聊天室WebGo
- SignalR來做實時Web聊天SignalRWeb
- Laravel+Layim+GatewayWorker 實現實時聊天功能LaravelGateway
- laravel5.4實現實時聊天室Laravel
- 怎樣在React-redux應用中使用Firebase實時資料庫ReactRedux資料庫
- iOS開發之微信聊天頁面實現iOS
- iOS流式即時通訊教程iOS
- 微信小程式-實現實時聊天功能 前端部分微信小程式前端
- xmpp實現的即時通訊聊天(一)
- xmpp實現的即時通訊聊天(二)
- iOS 微信聊天訊息的圖片氣泡實現iOS
- Agora iOS SDK-多人聊天GoiOS
- 即時聊天社交系統開發/聊天交友/ChatGPT社交聊天ChatGPT
- SignalR實現web線上即時聊天(C#)SignalRWebC#
- iOS中實現模糊效果教程iOS
- 即時通訊H5聊天系統IM聊天APP仿微信雙端android ios帶後臺H5APPAndroidiOS
- iOS實時卡頓監控iOS
- workman + Laravel auth 實現前後臺使用者實時聊天Laravel
- 使用 React+TypeScript+Firebase 實現的 Chrome Extension 總結ReactTypeScriptChrome
- 使用Deno和WebSockets構建實時聊天原始碼案例Web原始碼
- Node.js+websocket+mongodb實現即時聊天室Node.jsWebMongoDB
- 直播系統聊天技術(六):百萬人線上的直播間實時聊天訊息分發技術實踐
- 實用聊天技巧:怎麼聊天不冷場?
- ChatGPT社交聊天/即時聊天社交交友系統技術開發/聊天交友ChatGPT
- 教程 Redis+ flask+vue 線上聊天RedisFlaskVue
- [JavaScript+Firebase]基於Google Firebase的無後臺web端註冊與登入JavaScriptGoWeb
- 使用Firebase的中國開發者,想對Firebase團隊說些什麼?(有獎調查)
- supabase/supabase: 開源Firebase 替代方案
- 將Firebase加入我們的 APPAPP
- 即時聊天(IM)儲存方案
- iOS 實現時間線列表效果iOS
- iOS 給高仿微信新增直播聊天功能iOS
- iOS Swift 仿微信聊天圖片顯示iOSSwift