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

TamelaBunde 8年前
   <p>原文是基于Firebase 2.X构筑的匿名聊天室Demo,我在根据原文构筑时FireBase已经更新到3.X,谷歌对Firebase SDK做了不小的改动,所有的功能都由不同的类来操作,不再由Firebase类统一调度,这些改动致使原文某些地方变得不合时宜。</p>    <p>因此,我将自己根据原文构筑的基于3.X Firebase的流程及所遇到的坑穿插在原文中并且重新构筑了原文的部分代码。(有没有觉得构筑这两个字很有逼格?!)</p>    <p>现在主流的 App 都开始支持聊天功能了——你的 App 是不是也该支持一下?</p>    <p>但是,制作一个聊天工具确实不是一件简单的任务。我们不但缺乏现成的专门针对聊天的 UIKit 组件,还需要一个服务器来负责处理用户间的消息及对话。</p>    <p>幸运的是,我们可以使用一个优秀的框架:Firebase。它能为我们同步实时数据而无需编写任何服务端代码,同时还提供一个 JSQMessagesViewController 用于显示消息,这个 UI 可以和本地的消息应用相媲美。</p>    <p>在这个 Firebase 教程中,我们会创建一个可以进行匿名聊天的 App 叫做 ChatChat,如下图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/16401c332f988448fd1f504e530bc127.png"></p>    <p>最终你将学习到:</p>    <p>1、用 CocoaPods 安装 Firebase SDK 和 JSQMessagesViewController。</p>    <p>2、用 Firebase 数据库来同步实时数据。</p>    <p>3、让 Firebase 支持匿名登录。</p>    <p>4、用 JSQMessagesViewController 实现 UI。</p>    <p>5、当用户进行输入时,进行提示。</p>    <p>好了,接下来就开始吧!</p>    <h2><strong>开始</strong></h2>    <p>在开始本教程之前,请下载 <a href="/misc/goto?guid=4959664119075695529" rel="nofollow,noindex">开始项目</a> ,目前,它只完成了一个假的登录界面。</p>    <p>我们可以用CocoaPods安装Firebase SDK 和JSQMessagesViewController。如果你对 CocoaPods 不熟,可以参考我们的 <a href="/misc/goto?guid=4959637705899084219" rel="nofollow,noindex">CocoaPods Swift 教程</a> 。</p>    <p>打开终端,进入项目文件夹路径。在项目根路径下,新建一个 Podfile 文件。在文件中加入对 Firebase SDK 和 JSQMessagesViewController 的依赖,如下所示:</p>    <pre>  <code class="language-objectivec">platform :ios, "9.0"  use_frameworks!  target 'ChatChat' do    pod 'Firebase'    pod 'JSQMessagesViewController'  end</code></pre>    <p>保存 Podfile 文件,然后用以下命令安装依赖:</p>    <pre>  <code class="language-objectivec">pod install</code></pre>    <p>译者注:由于众所周知的原因,CocoaPods 对于国内用户来说并不友好,经常出现各种无法 pod install 的情况。如果是这样,你必须手动安装这两个库了。关于 Firebase 的手动安装,请看这里。关于 JSQMessagesViewController 的手动安装,你需要从 github 下载 JSQMessagesViewController 和 JSQSystemSoundPlayer 这两个库的源文件然后添加到项目里,并改正 JSQSystemSoundPlayer+JSQMessages.m 中的两个错误即可,然后在桥接头文件中导入相应的 .h 文件(包括并不限于):</p>    <pre>  <code class="language-objectivec">#import "JSQMessage.h"  #import "JSQMessagesBubbleImage.h"  #import "JSQMessagesViewController.h"  #import "JSQMessagesBubbleImageFactory.h"  #import "UIColor+JSQMessages.h"  #import "JSQMessageAvatarImageDataSource.h"  #import "JSQSystemSoundPlayer+JSQMessages.h"</code></pre>    <p>茄子注:个人使用CocoaPod安装FireBase时惨遭谷歌翻脸,然后在手动集成时又被官方文档坑了,在此将手动集成时的一些注意点根据自己手动集成的流程说明。</p>    <p>首先,你可以在 <a href="/misc/goto?guid=4959715721552498388" rel="nofollow,noindex">这里</a> 下载Firebase SDK</p>    <p><img src="https://simg.open-open.com/show/2f379afcafc9d57cdccf64341b02bf66.png"></p>    <p>选择其中一些组件或者像我一样直接把整个包丢进项目中。</p>    <p>接着第三步,将ObjC链接器标志添加到Other Linker Settings中。</p>    <p><img src="https://simg.open-open.com/show/401a478cc5fb1b456d863c9bc865f3ac.png"></p>    <p><img src="https://simg.open-open.com/show/af982cbd13ac84b3fbb8c52db9ca0c44.png"></p>    <p>这一步需要注意的是,你需要在Project中及使用到Firebase的Targets中都将ObjC链接器标志添加到Other Linker Settings中。</p>    <p>接着,Run一下,是否Crash及控制台报告如下:</p>    <pre>  <code class="language-objectivec">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.</code></pre>    <p>我们需要将注册FireBase项目时获得的GoogleService-Info.plist中间中的REVERSED_CLIENT_ID添加到Info -> URL Types -> URL Schemes中,如下图:</p>    <p><img src="https://simg.open-open.com/show/1916b8d72ac5e82132d9c88194ed5d22.png"></p>    <p>再次运行你就能看到正常的运行画面:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a2d59bf22bc0bab10818d6cbd4972def.png"></p>    <p>注意:在接下来的教程中,你每次编译和运行都会看到这个界面。点击“匿名登录”又会切换到另一个界面。目前点击按钮没有什么用处,但随后我们就会实现它。</p>    <p>如果你第一次接触 Firebase,你需要创建一个账号。不用担心—— 它非常简单,而且完全是免费的,不需要信用卡。</p>    <p>注意:关于如何注册 Firebase 的完整步骤,请看我们的 <a href="/misc/goto?guid=4959664119193590273" rel="nofollow,noindex">Firebase 入门教程</a> 。</p>    <h2><strong>注册 Firebase 账号</strong></h2>    <p>进入 <a href="/misc/goto?guid=4959664119276080669" rel="nofollow,noindex">Firebase 注册页面</a> ,创建一个账号,然后创建一个 Firebase App。就本教程而言,你需要使用实时数据库和身份认证服务。</p>    <h2><strong>开启匿名认证</strong></h2>    <p>Firebase 允许用户通过 email 地址或社交账号进行登录,但也提供匿名登录功能,后者会给每个用户分配一个唯一的 ID 但不需要用户的输入任何个人信息。</p>    <p>匿名认证就好比说:“我不知道你是谁,我只知道你是一个人。”。对于访问账户或者使用用户来说,这是非常方便的。这对于本教程来说非常适合,因为 ChatChat 中所有用户都是匿名的。</p>    <p>要开启匿名认证,你需要进入你的 <a href="/misc/goto?guid=4959715721712678944" rel="nofollow,noindex">Firebase 项目</a> ,选择 Auth 标签,若是初次创建则会指引你选择登陆方法,启用匿名登陆即可。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6b43aa8d327002f2c09bbd0dea0fb5f3.png"></p>    <p><img src="https://simg.open-open.com/show/9a9ec7dd6db59390eb9ca192a9d39a9f.png"></p>    <p>这样,你就开启了超级隐身模式,也就是匿名认证——很爽吧:]</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d623b74671f092a1ffb357fd2e8be3c2.png"></p>    <h2><strong>登陆</strong></h2>    <p>打开 LoginViewController.Swift,加入:</p>    <pre>  <code class="language-objectivec">import Firebase</code></pre>    <p>要登入聊天室,需要连接到 Firebase 数据库。在 LoginViewController.swift 中加入:</p>    <pre>  <code class="language-objectivec">class LoginViewController: UIViewController {        var ref: FIRDatabaseReference!        override func viewDidLoad() {          super.viewDidLoad()          ref = FIRDatabase.database().referenceFromURL("https://fir-demo-43879.firebaseio.com/")          }</code></pre>    <p>上面的代码是什么意思?</p>    <p>首先,定义一个属性,存放 Firebase database 的引用。</p>    <p>然后,用你的 Firebase App URL 创建一个 Firebase 数据库连接并赋给这个属性。</p>    <p>如果你不知道你的 Firebase App URL 是什么,就像上一步一样点击Database即可 :</p>    <p><img src="https://simg.open-open.com/show/06116e42cac18486aa89e488ea9a5553.png"></p>    <p>茄子注,虽然Firebase没有向我们提供Database的源代码,但从其API上大致可以猜测一二,使用URL进行首次连接,接着在注销之前便会无限制地互发数据,这点上与WebSocket的机制非常相似,我们有理由猜测FireDatabase的底层是利用WebSocket的概念实现的。</p>    <p>要登录一个用户,可以在数据库引用对象上调用 authAnonymouslyWithCompletionBlock(_:)。</p>    <p>在 loginDidTouch(_:) 方法中添加代码:</p>    <pre>  <code class="language-objectivec">@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      })    }</code></pre>    <p>在这个方法中,完成了如下工作:</p>    <p>1、调用 FiRAuth的单例 的 authAnonyouslyWithCompletionBlock(_:) 方法以匿名方式登录一个用户。</p>    <p>2、检查是否认证失败。</p>    <p>3、在闭包中,调用 segue 跳转到 ChatViewController。</p>    <p>闭包回调中的user包含了用户的唯一标示符及是否匿名登陆等重要信息,当我们在使用到时可以通过:</p>    <pre>  <code class="language-objectivec">FIRAuth.auth()?.currentUser</code></pre>    <p>来获得相应信息。</p>    <p>茄子注:匿名登陆并不会随时改变,就像一般的账户一样通过一个refreshToken来实现长期登陆,这个属性被加密成无规则字符串。</p>    <h2><strong>创建聊天界面</strong></h2>    <p>JSQMessagesViewController 是一个 UICollectionViewController 的封装,为聊天进行了专门的定制。</p>    <p>本教程将主要介绍 5 个步骤:</p>    <p>1、创建消息数据</p>    <p>2、创建带背景色的消息气泡</p>    <p>3、删除头像</p>    <p>4、改变 UICollectionViewCell 的文字颜色</p>    <p>5、提示用户正在输入</p>    <p>几乎每个步骤都需要覆盖一些方法。JSQMessagesViewController 使用了JSQMessagesCollectionViewDataSource 协议,因此我们需要覆盖协议的默认实现就可以了。</p>    <p>注意:关于 JSQMessagesCollectionViewDataSource 的更多内容,请参考 <a href="/misc/goto?guid=4959664119357753566" rel="nofollow,noindex">这里</a> 。</p>    <p>打开 ChatViewController.swift 导入 Firebase 和JSQMVC :</p>    <pre>  <code class="language-objectivec">import Firebase  import JSQMessagesViewController</code></pre>    <p>将父类从 UIViewController 类修改 JSQMessagesViewController 类:</p>    <pre>  <code class="language-objectivec">class ChatViewController: JSQMessagesViewController {</code></pre>    <p>现在 ChatViewController 继承了 JSQMessagesViewController,我们可以设置 senderId 和 senderDisplayName 的初始值了,这样 App 才能唯一识别消息的发送者——否则它无从知道发送者是谁。</p>    <p>在 LoginViewController 中,我们用 user 将用户信息传递给 ChatViewController(通过 prepareForSegue 方法)。</p>    <p>在 LoginViewController 中添加方法:</p>    <pre>  <code class="language-objectivec">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 = ""      }</code></pre>    <p>在上面的代码中:</p>    <p>1、获取 segue 的目标 View Controller 并转换成 UINavigationController。</p>    <p>2、将 Navigation Controller 的第一个 View Controller 转换成ChatVC。</p>    <p>3、将本地用户的 ID 赋给 chatVc.senderId,这是 JSQMVC 用于处理消息的客户端 ID。</p>    <p>4、将 chatVc.senderDisplayName 设为空字符串,因为我们的聊天室是匿名登录的。</p>    <p>注意每个 App 会话中,我们只会有一个匿名的会话。每次重启 App 之后,你都会获得一个新的、唯一的匿名用户。如果你重启模拟器,你会看到另外一个用户 ID。</p>    <p>茄子注:上面那段话存在于原译文之中,我没有与英文原文核实是否英文原文存在。有可能是Firebase的机制变化了,我们可以从不断重启模拟器、打印user.uid及通过Firebase项目的Auth页面查看验证得到user.uid没有发生变化,因此每次重启App之后都会得到一个新的匿名用户是不正确的。</p>    <p>运行程序,检查你的 App 是否运行在超级隐身模式:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e90904818c5c495d8942d620b03e8705.png"></p>    <p>通过简单地继承下 JSQMessagesViewController,你就获得了一个完整的聊天 UI。太爽了!</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0335a5bc32b21a71d531f2de441d29a3.png"></p>    <h2><strong>创建数据源及委托</strong></h2>    <p>现在,我们有了一个聊天 UI,你可能很想在上面显示点什么了。但首先,你需要注意几件事情。</p>    <p>要显示聊天消息,我们需要一个数据源,即一个实现了 JSMessageData 协议的对象并实现一些委托方法。我们可以自己定义一个类来实现 JSQMessageData 协议,也可以使用现成的 JSQMessage 类。</p>    <p>在 ChatViewController 头部,定义一个属性:</p>    <pre>  <code class="language-objectivec">// MARK: Properties  var messages = [JSQMessage]()</code></pre>    <p>messages 属性是一个数组,存储了多个 JSQMessage 实例。</p>    <p>在 ChatViewController 中,实现 2 个委托方法:</p>    <pre>  <code class="language-objectivec">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  }</code></pre>    <p>这两个委托方法可能并不陌生。第一个方法和 collectionView( <em>:cellForItemAtIndexPath:) 方法一样,只不过返回类型变成了 JSQMessageData 而已。第二个则和 collectionView(</em> :numberOfItemsInSection:) 方法完全一样。</p>    <p>还有几个必须实现的委托方法,用于提供消息数据、气泡图片以及头像。提供消息数据的方法已经实现了,接下来就是提供气泡和头像的方法。</p>    <h2><strong>气泡颜色</strong></h2>    <p>在 Collection View 中,消息文本显示在一个简单的图片背景之上。有两种类型的消息:收到的消息和发出的消息。发出的消息靠右侧显示而收到的消息则靠左显示。</p>    <p>在 ChatViewController 中,添加两个属性:</p>    <pre>  <code class="language-objectivec">var outgoingBubbleImageView: JSQMessagesBubbleImage!  var incomingBubbleImageView: JSQMessagesBubbleImage!</code></pre>    <p>然后添加方法:</p>    <pre>  <code class="language-objectivec">private func setupBubbles() {    let factory = JSQMessagesBubbleImageFactory()    outgoingBubbleImageView = factory.outgoingMessagesBubbleImageWithColor(        UIColor.jsq_messageBubbleBlueColor())    incomingBubbleImageView = factory.incomingMessagesBubbleImageWithColor(        UIColor.jsq_messageBubbleLightGrayColor())  }</code></pre>    <p>JSQMessagesBubbleImageFactory 有创建聊天气泡的方法。在 JSQMessagesViewController 中有一个 Category,允许我们使用原生消息 App 中消息气泡所使用的颜色。</p>    <p>通过 bubbleImageFactory.outgoingMessagesBubbleImageWithColor() 和 bubbleImageFactory.incomingMessagesBubbleImageWithColor() 方法,我们可以创建出接收消息和发出消息的气泡图片。</p>    <p>然后,在 viewDidLoad() 方法中调用这个 setupBubbles() 方法:</p>    <pre>  <code class="language-objectivec">override func viewDidLoad() {    super.viewDidLoad()    title = "ChatChat"    setupBubbles()  }</code></pre>    <h2><strong>设置气泡图片</strong></h2>    <p>要为每条消息设置颜色气泡,我们需要覆盖 JSQMessagesCollectionViewDataSource 协议中的一个方法。</p>    <p>collectionView(_:messageBubbleImageDataForItemAtIndexPath:) 方法会要求我们为 CollectionView 中的每条消息数据提供一个与之相对应的 JSQMessageBubbleImageDataSource。这个方法正是我们设置气泡图片的好时机。</p>    <p>在 ChatViewController 添加方法:</p>    <pre>  <code class="language-objectivec">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    }  }</code></pre>    <p>逐行分析上面的代码:</p>    <p>根据 NSIndexPath 检索出对应的消息数据。</p>    <p>判断这条消息是否是本客户端所发出的,如果是,返回“发出消息”的 Image View。</p>    <p>如果不是,则返回“接收消息”的 ImageView。</p>    <p>在运行程序之前的最后一个步骤,是删除头像以及头像删除后留下的空白。</p>    <p>在 ChatViewController 中加入方法:</p>    <pre>  <code class="language-objectivec">override func collectionView(collectionView: JSQMessagesCollectionView!,       avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {    return nil  }</code></pre>    <p>然后,在 viewDidLoad() 加入代码:</p>    <pre>  <code class="language-objectivec">// No avatars  collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSizeZero  collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero</code></pre>    <p>JSQMessagesViewController 支持头像显示,但我们用不到(或者不想),因为我们的 App 是一个匿名的聊天室。要删除头像显示,只需要在询问每条消息的头像时返回一个 nil 并将头像的 size 指定为 CGSizeZero,即“大小为 0”。</p>    <p>接下来开始对话并发送几条消息!</p>    <h2><strong>发送消息</strong></h2>    <p>在 ChatViewController 中增加方法:</p>    <pre>  <code class="language-objectivec">func addMessage(id: String, text: String) {    let message = JSQMessage(senderId: id, displayName: "", text: text)    messages.append(message)  }</code></pre>    <p>这个工具方法用于创建一条新的 displayName 为空的 JSQMessage,然后将它添加到数据源中。</p>    <p>在 viewDidAppear() 方法中硬编码几条消息以便我们能真正看到它们:</p>    <pre>  <code class="language-objectivec">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()  }</code></pre>    <p>运行程序,你会看到会话窗口中显示了几条聊天消息:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c6986c3be32d1cc9e26a7e6d9ba465ca.png"></p>    <p>呃,接收消息中的文字也太不显眼了。最好将它设置成黑色。</p>    <p>消息气泡中的文字</p>    <p>正如你所见,JSQMessagesViewController 中几乎每样东西都和一个委托方法有关。要设置文字颜色,我们可以使用经典的 collectionView(_:cellForItemAtIndexPath:) 方法。</p>    <p>在 ChatViewController 中加入一个方法:</p>    <pre>  <code class="language-objectivec">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  }</code></pre>    <p>如果消息是本客户端用户所发,则文字颜色为白色,否则文字颜色为黑色。</p>    <p>运行程序,接收消息的文字变成黑色的了:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/53ea3b947b9638916395d8117b4caf9e.png"></p>    <p>哇——看起来养眼多了!是时候让它真正使用 Firebase 了。</p>    <h2><strong>Firebase 数据结构</strong></h2>    <p>在开始让数据实时同步之前,先花点时间来看看数据结构。</p>    <p>Firebase 数据库是 NoSQL 数据库,也就是说,在 Firebase 数据库中的每个对象都是 JSON 对象,这个 JSON 对象的每一个 key 都可以通过不同的 URL 来访问。</p>    <p>举一个例子,你的数据很可能是由这样一个 JSON 构成:</p>    <pre>  <code class="language-objectivec">{    // 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"      },    }  }</code></pre>    <p>Firebase 数据库支持“不规范的”数据结构,因此在每个 message 中都包含 senderId 是可以的。“不规范”的数据结构会导致一些数据冗余,但优点是检索数据的速度更快。权衡下来——我们还是可以接受的。</p>    <p>创建 Firebase 引用</p>    <p>在 ChatViewController.swift 中增加属性:</p>    <pre>  <code class="language-objectivec">let rootRef = FIRDatabase.database().referenceFromURL("https://<my-firebase-app>.firebaseio.com/messages/")      var messageRef:FIRDatabaseReference!</code></pre>    <p>译者注:将 \ 替换成你自己的 Firebase App ID。</p>    <p>在 viewDidLoad() 方法中,初始化 messageRef:</p>    <pre>  <code class="language-objectivec">override func viewDidLoad() {          super.viewDidLoad()          title = "ChatChat"          setupBubbles()          collectionView.collectionViewLayout.incomingAvatarViewSize = CGSize.zero          collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero          messageRef = rootRef.child("messages")      }</code></pre>    <p>我们创建了一个 rootRef 对象用于连接 Firebase 数据库。然后用 child() 方法创建了一个 messageRef 对象,这个方法可用于创建下级引用。</p>    <p>不要奇怪,创建另一个引用并不意味着就需要创建新的连接。所有的引用其实可以共享同一个 Firebase 数据库连接。</p>    <h2><strong>发送消息</strong></h2>    <p>你可能迫不及待地想点击“Send”按钮了,如果你这样做,你会让 App 崩溃。现在,你已经连上了 Firebase 数据库,你可以真正地去发送几条消息了。</p>    <p>首先,删除 ChatViewController 中 viewDidAppear(_:) 方法中的测试消息。</p>    <p>然后,覆盖下面的这个方法。这个方法允许发送按钮将一条消息保存到 Firebase 数据库:</p>    <pre>  <code class="language-objectivec">//发送消息      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()      }</code></pre>    <p>这个方法负责:</p>    <p>1、通过 childByAutoId(),我们获得一个子对象引用,该对象有一个自动创建的唯一 key。</p>    <p>2、用一个字典来保存消息。一个 [String:AnyObject] 足以表示一个 JSON 对象。</p>    <p>3、将字典保存到新的子引用中。</p>    <p>4、播放一个经典的代表“消息已发送”的声音。</p>    <p>5、完成“发送”动作,将输入栏置空。</p>    <p>运行程序,打开你的 Firebase App Dashboard,点击 Data 栏。在 App 中发送一条消息,你立即会在 Dashboard 中看到这条消息显示。</p>    <p><img src="https://simg.open-open.com/show/af17e0405128aea599ff01d18464ac82.gif"></p>    <p>茄子注:抱歉,实在不想重制gif,麻烦得要死ORZ。</p>    <p>成功了!你已经很“专业”地将消息保存到了 Firebase 数据库。这个消息还没有显示到 iPhone 上,但我们接下来就会这样做。</p>    <h2><strong>与 Firebase 保持实时同步</strong></h2>    <p>每当我们改变 Firebase 数据库中的数据,数据库就会将修改 push 给每个已经连接的 App 上。Firebase 的数据同步机制分为三个部分:URL、事件和快照。</p>    <p>例如,你可以用这种方式来监听新的消息:</p>    <pre>  <code class="language-objectivec">let rootRef = FIRDatabase.database().referenceFromURL("https://fir-demo-43879.firebaseio.com/")          rootRef.observeEventType(.ChildChanged) { (snapshot:FIRDataSnapshot) in              print(snapshot.value)          }</code></pre>    <p>这段代码主要是:</p>    <p>通过 Firebase App URL,我们创建了一个 Firebase 数据库引用。我们指定的这个 URL 指向了我们想读取的数据。</p>    <p>用 FEventType.ChildAdded 参数调用 observeEventType(_:FEventType:) 方法。在每当位于该 URL 的对象添加了新的子对象时都会触发一次 child-added 事件。</p>    <p>闭包中会传入一个 FDataSnapshot 对象,这个对象中会包含有相应的数据以及一些有用的方法。</p>    <h2><strong>同步数据源</strong></h2>    <p>看到了吧,和 Firebase 保持数据同步是非常简单的,接下来是和数据源进行对接。</p>    <p>在 ChatViewController 中增加一个方法:</p>    <pre>  <code class="language-objectivec">//监听消息      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()          }      }</code></pre>    <p>这个方法主要是:</p>    <p>1、创建一个查询,限制要同步的数据为 25 条记录。</p>    <p>2、监听指定位置上的 .ChildAdded 事件,当结果集中有新的子对象添加和即将添加时触发此事件。</p>    <p>3、从 snapshot.value 上读取 senderId 和 text。</p>    <p>4、调用 addMessage() 方法将新消息添加到数据源。</p>    <p>5、通知 JSQMessagesViewControllers(),收到一条消息。</p>    <p>然后,在 viewDidAppear(_:) 方法中调用 observeMessages() 方法:</p>    <pre>  <code class="language-objectivec">override func viewDidAppear(animated: Bool) {    super.viewDidAppear(animated)    observeMessages()  }</code></pre>    <p>运行程序,你会看到新发送的消息会附加到已经发送的消息之后:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a93e2a1ea2a846718c5257b3b1581244.png"></p>    <p>恭喜你!你已经有了一个实时聊天 App!现在该做一些更神奇的事了,比如说提示用户正在输入。</p>    <h2><strong>当用户正在输入时进行提示</strong></h2>    <p>这个 App 最酷的特性之一是提示“用户正在输入…”信息。会有一个小气泡弹出,告诉你用户正在键盘上敲击。这个提示非常重要,因为它让我们减少了许多诸如“还在吗?”之类的不必要的消息。</p>    <p>有许多方法可以检测是否正在输入,但 textViewDidChange(_:textView:) 方法是最好的方法。例如:</p>    <pre>  <code class="language-objectivec">override func textViewDidChange(textView: UITextView) {    super.textViewDidChange(textView)    // If the text is not empty, the user is typing    print(textView.text != "")  }</code></pre>    <p>要判断用户是否正在敲击键盘,只需要检查 textView.text 的值。如果这个值不为空,我们就可以认为用户正在输入。</p>    <p>通过 Firebase,我们可以在用户输入时向 Firebase 数据库更新状态。然后,通过从数据库检索这个状态,显示“用户正在输入”的提示。</p>    <p>首先在 ChatViewController 中增加几个属性:</p>    <pre>  <code class="language-objectivec">var userIsTypingRef:FIRDatabaseReference! //1      private var localTyping = false //2      var isTyping: Bool {          set{              //3              localTyping = newValue              userIsTypingRef.setValue(newValue)          }          get{              return localTyping          }      }</code></pre>    <p>这些属性分别用于:</p>    <p>1、一个 FIRDatabaseReference 引用,用于存储当前用户是否正在输入。</p>    <p>2、一个私有属性,用于记录当前用户是否正在输入。</p>    <p>3、一个计算属性,通过简单地给这个属性赋值,就可以实时修改 userIsTypingRef。</p>    <p>在 ChatViewController 添加一个方法:</p>    <pre>  <code class="language-objectivec">//监听输入      func observeIsTyping() {          let typingIndicatorRef = rootRef.child("typingIndicator")          userIsTypingRef = typingIndicatorRef.child(senderId)          userIsTypingRef.onDisconnectRemoveValue()      }</code></pre>    <p>这个方法创建了一个引用,指向 URL “/typingIndicator”,这个地址用于更新用户的输入状态。当用户退出后,我们不需要这个数据了,因此我们可以用 onDiscounnectRemoveValue() 指定,当用户离开则删除该数据。</p>    <p>在 viewDidAppear(_:) 方法中调用 observeTyping() 方法:</p>    <pre>  <code class="language-objectivec">override func viewDidAppear(animated: Bool) {    super.viewDidAppear(animated)    observeMessages()    observeTyping()  }</code></pre>    <p>在 ChatViewController 中添加 textViewDidChange(_:textView:) 方法,修改 isTyping 的值:</p>    <pre>  <code class="language-objectivec">override func textViewDidChange(textView: UITextView) {    super.textViewDidChange(textView)    // If the text is not empty, the user is typing    isTyping = textView.text != ""  }</code></pre>    <p>最后,在 didPressSendButton(_:withMessageText:senderId:senderDisplayName:date:) 方法最后重置输入提示:</p>    <pre>  <code class="language-objectivec">isTyping = false</code></pre>    <p>运行程序,在 Firebase App Dashboard 中观察数据。当你输入消息内容时,你会看到这个用户的 typingIndicator 会随之改变:</p>    <p><img src="https://simg.open-open.com/show/6d1eae6d9d555b1a5b9b4cd62bcc6f15.gif"></p>    <p>噢!现在你已经能够知道用户什么时候输入了!让我们来显示这个提示。</p>    <h2><strong>查询哪些用户正在输入</strong></h2>    <p>“用户正在输入”应当在用户输入的时候显示,但不应当计算本地用户。我们没有必要知道(我们已经知道)当前本地用户是否正在输入。</p>    <p>用一个 Firebase 查询,我们可以知道当前正在输入的所有用户。在 ChatViewController 中加入一个属性:</p>    <pre>  <code class="language-objectivec">var userTypingQuery:FIRDatabaseQuery!</code></pre>    <p>然后,修改 observeTyping() 为:</p>    <pre>  <code class="language-objectivec">//监听输入      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)              }          }      }</code></pre>    <p>在代码中,我们:</p>    <p>1、初始化一个查询,用于查询当前正在输入的用户。这一句相当于“喂,Firebase,去 /typingIndicator (这是一个对象,包含了若干键值对)下面看看,告诉我哪些键值对的值是 true。”</p>    <p>2、用 .Value 监听改变,一旦这些值发生任何变化,就会立即通知你。</p>    <p>3、检查结果中有多少用户正在输入。如果只有一个,则再检查这个用户是不是本地用户,如果是,不显示提示。</p>    <p>4、如果有不止一个用户,而且本地用户并没有在输入,则需要显示输入提示。最后,调用 scrollToBottomAnimated(_:animated:) 方法,确保输入提示能够被看到。</p>    <p>在运行程序之前,还需要一台物理设备,因为这个测试需要两台设备。用模拟器扮演一个用户,而物理设备扮演另外一个用户。</p>    <p>在这两台设备上(一台是模拟器,一台是物理设备)运行程序,当一个用户在输入时,你可以看到提示显示了(注意气泡中有省略号):</p>    <p><img src="https://simg.open-open.com/show/9b743e8e7948aabe4313459ef6287862.gif"></p>    <p>哇!你创建了一个伟大的、酷炫的、实时的、带用户输入提示的聊天 App。人生如此,当浮一大白!</p>    <p>在这个教程中,你学会了如何使用 Firebase 和 JSQMessagesViewController,但仍然还有许多事情可做,比如 1 对 1 聊天,社交账号登录以及头像显示。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/98eb3356593b</p>    <p> </p>