原文
原文是基于Firebase 2.X构筑的匿名聊天室Demo,我在根据原文构筑时FireBase已经更新到3.X,谷歌对Firebase SDK做了不小的改动,所有的功能都由不同的类来操作,不再由Firebase类统一调度,这些改动致使原文某些地方变得不合时宜。
因此,我将自己根据原文构筑的基于3.X Firebase的流程及所遇到的坑穿插在原文中并且重新构筑了原文的部分代码。(有没有觉得构筑这两个字很有逼格?!)
这篇文章是基于Firebase 3.X的简单教程,若是不需要的就右上角吧(=゚ω゚)=。
现在主流的 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 3.X代码在此下载
在这个教程中,你学会了如何使用 Firebase 和 JSQMessagesViewController,但仍然还有许多事情可做,比如 1 对 1 聊天,社交账号登录以及头像显示。
要进一步完善此 App,请参考 Firebase iOS 文档。
如果想了解如何用社交账号登录用户,请参考Firebase 用户认证指南,其中包括了 Twitter、Google、Facebook 和 GitHub。
我希望你喜欢这篇教程,如果有任何问题,请在下面的评论中(非匿名的,而且支持头像)提出。
茄子注:边写Demo边写博文,到最后才发现除了手动安装的一些坑及Firebase 3.X API 的改动外没太大变化,但也写完了还是发出来吧。