Firebase 教程 —— 一个实时聊天室

原文
  原文是基于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 的改动外没太大变化,但也写完了还是发出来吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容