Firebase Tutorial: Real-time Chat
来自: http://www.raywenderlich.com/122148/firebase-tutorial-real-time-chat
Hey! Let’s chat.
It seems like every major app out there has a chat feature — and yours should be no different!
However, creating a chat tool can seem like a daunting task. There’s no native UIKit controls designed specifically for chat, and you’ll need a server to coordinate the messages and conversations between users.
Fortunately, there’s some great frameworks out there to help you: Firebase lets you synchronize real time data without writing a line of server code, while JSQMessagesViewController gives you a messaging UI that’s on par with the native Messages app.
In this Firebase tutorial you’ll build an anonymous chat app named ChatChat that looks like the following:
Along the way, you’ll learn how to do the following:
- Set up the Firebase SDK and JSQMessagesViewController with CocoaPods.
- Synchronize data in real time with the Firebase database.
- Authenticate anonymously with Firebase.
- Leverage JSQMessagesViewController for a complete chat UI.
- Indicate when a user is typing.
Enough chit-chat — it’s time to get started! :]
Getting Started
To get started with this Firebase tutorial, download the starter project here ; at present, it contains a simple dummy login screen.
You’ll use CocoaPods to install both the Firebase SDK and JSQMessagesViewController. If you’re new to CocoaPods, check out our Cocoapods with Swift tutorial to get up and running.
Open Terminal at the project’s folder location. Create an empty Podfile at the project’s root folder, then inside the Podfile declare the Firebase SDK and JSQMessagesViewController as dependencies like so:
platform :ios, "9.0" use_frameworks! target 'ChatChat' do pod 'Firebase' pod 'JSQMessagesViewController'end
Save the Podfile and run the following command to install your dependencies:
pod install
Once the packages have installed, open the newly created ChatChat.xcworkspace and build and run. You should see the following:
Note: For the remainder of this Real time Chat tutorial, you’ll see this view each time you build and run. Tap Login anonymously each time to move to the next view. At present, the button does nothing, but you’ll fix that shortly.
If you’re new to Firebase you’ll need to create an account. Don’t worry – this is easy and totally free, as in credit-card free.
Note: For a detailed walkthrough on setting up Firebase, see the Getting Started with Firebase tutorial .
Create a Firebase Account
Head to the Firebase signup site , create an account, and then create a Firebase app. For this Real time Chat tutorial, you’ll use the real-time database and authentication services of your new app.
Enabling Anonymous Authentication
Firebase lets users login through email or social accounts, but it can also authenticate users anonymously, which gives you a unique identifier for a user without knowing any information about them.
Anonymous authentication is like saying, “I don’t know who you are, but I know you’re there.” It’s great for situations like guest accounts and trial runs. It also fits this tutorial perfectly, because ChatChat is all about anonymity.
To set up anonymous authentication, open the Firebase App Dashboard , select the Login & Auth tab, click Anonymous , and tick the Enable Anonymous User Authentication checkbox:
Just like that, you’ve enabled super secret stealth mode ! Okay, so it’s really just anonymous authentication, but hey — it’s still cool. :]
Logging In
Open LoginViewController.swift and add the following import:
import Firebase
To login to chat, the app will need to open a connection to the Firebase database. Add the following code to LoginViewController.swift :
class LoginViewController: UIViewController { // MARK: Properties var ref: Firebase! // 1 override func viewDidLoad() { super.viewDidLoad() ref = Firebase(url: "https://<my-firebase-app>.firebaseio.com") // 2 } }
Here’s what’s going on in the code above:
- First, you specify a Firebase database reference as a property.
- Then, using your Firebase App URL, you initialize the property and create a connection to the Firebase database.
If you’re not sure what your Firebase App URL is, you can find it in your Firebase App Dashboard:
To log a user in, you call authAnonymouslyWithCompletionBlock(_:) on your database reference.
Add the following code to loginDidTouch(_:) :
@IBAction func loginDidTouch(sender: AnyObject) { ref.authAnonymouslyWithCompletionBlock { (error, authData) in // 1 if error != nil { print(error.description); return } // 2 self.performSegueWithIdentifier("LoginToChat", sender: nil) // 3 } }
Here’s a run-down of what you’re doing in this code:
- From the ref , call authAnonymouslyWithCompletionBlock(_:) to log a user in anonymously.
- Check for an authentication error.
- Inside of the closure, trigger the segue to move to ChatViewController .
You might be wondering why this code ignores authData . While this callback parameter contains a unique identifier for the user, you don’t need to worry about passing it around. After you’ve authenticated a user, you’ll have access to authData as a property on any Firebase database reference in the following way:
// if authenticated, it will print the current user information print(ref.authData)
Creating the Chat Interface
JSQMessagesViewController is a souped up UICollectionViewController that’s customized for chat.
This Firebase tutorial will focus on the following five things:
- Creating message data
- Creating colored message bubbles
- Removing avatar support
- Changing the text color of a UICollectionViewCell
- Indicating when a user is typing
Almost everything you’ll need to do requires that you override methods. JSQMessagesViewController adopts the JSQMessagesCollectionViewDataSource protocol, so you only need to override the default implementations.
Note: For more information on JSQMessagesCollectionViewDataSource , check out the documentation here .
Open up ChatViewController.swift and import Firebase and JSQMessagesViewController as shown below:
import Firebase import JSQMessagesViewController
Change the subclass from UIViewController to JSQMessagesViewController :
class ChatViewController: JSQMessagesViewController {
Now that ChatViewController extends JSQMessagesViewController , you’ll need to set initial values for senderId and senderDisplayName so the app can uniquely identify the sender of the messages — even if it doesn’t know specifically who that person is.
In LoginViewController , you can use ref.authData to populate the user’s specific data on the ChatViewController as the segue is being prepared.
Add the following method to LoginViewController :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { super.prepareForSegue(segue, sender: sender) let navVc = segue.destinationViewController as! UINavigationController // 1 let chatVc = navVc.viewControllers.first as! ChatViewController // 2 chatVc.senderId = ref.authData.uid // 3 chatVc.senderDisplayName = "" // 4 }
Here’s what you’re doing in the code above:
- Retrieve the destination view controller from segue and cast it to a UINavigationController .
- Cast the first view controller of the UINavigationController as ChatViewController .
- Assign the local user’s ID to chatVc.senderId ; this is the local ID that JSQMessagesViewController uses to coordinate messages.
- Make chatVc.senderDisplayName an empty string, since this is an anonymous chat room.
Note that you only get one anonymous authentication session per app session. Each time you restart the app, you’ll get another, unique, anonymous user. As you start and stop the simulator, you’ll see different user IDs.
Build and run to take a look at your app in super-secret stealth mode:
By simply inheriting from JSQMessagesViewController you get a complete chat UI. Fancy chat UI win!
Setting Up the Data Source and Delegate
Now that you’ve seen your new awesome chat UI, you’re probably excited to start displaying messages. But before you do that, you have to take care of a few things first.
To display messages, you need a data source to provide objects that conform to the JSQMessageData protocol and you need to implement a number of delegate methods. You could create your own class that conforms the JSQMessageData protocol, but you’ll use the built-in JSQMessage class that is already provided.
At the top of ChatViewController , define the following property:
// MARK: Properties var messages = [JSQMessage]()
messages is an array to store the various instances of JSQMessage in your app.
Still in ChatViewController , implement the two delegate methods below:
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 }
You’re probably no stranger to the two types of delegate methods above. The first is much like collectionView(_:cellForItemAtIndexPath:) , but for message data. The second is the same old collectionView(_:numberOfItemsInSection:) .
You’ll also need to implement the required delegate methods for message data, message bubble images, and avatar images. The message data delegates have been taken care of, so you’ll tackle the bubble and avatar images next.
Message Bubble Colors
The messages displayed in the collection view are simply images with text overlaid. There are two types of messages: outgoing and incoming . Outgoing messages are displayed to the right and incoming messages on the left.
In ChatViewController , add the following properties:
var outgoingBubbleImageView: JSQMessagesBubbleImage! var incomingBubbleImageView: JSQMessagesBubbleImage!
Then add the following method:
private func setupBubbles() { let factory = JSQMessagesBubbleImageFactory() outgoingBubbleImageView = factory.outgoingMessagesBubbleImageWithColor( UIColor.jsq_messageBubbleBlueColor()) incomingBubbleImageView = factory.incomingMessagesBubbleImageWithColor( UIColor.jsq_messageBubbleLightGrayColor()) }
JSQMessagesBubbleImageFactory has methods that create the images for the chat bubbles. There’s even a category provided by JSQMessagesViewController that creates the message bubble colors used in the native Messages app.
Using the methods bubbleImageFactory.outgoingMessagesBubbleImageWithColor() and bubbleImageFactory.incomingMessagesBubbleImageWithColor() , you can create the images for outgoing and incoming messages respectively.
Add the following call to setupBubbles() in viewDidLoad() :
override func viewDidLoad() { super.viewDidLoad() title = "ChatChat" setupBubbles() }
And with that, you have the image views needed to create outgoing and incoming message bubbles! Before you get too excited, you’ll need to implement the delegate methods for the message bubbles.
Setting the Bubble Images
To set the colored bubble image for each message, you’ll need to override a method of JSQMessagesCollectionViewDataSource .
collectionView(_:messageBubbleImageDataForItemAtIndexPath:) asks the data source for the message bubble image data that corresponds to the message data item at indexPath in the collectionView . This is exactly where you set the bubble’s image.
Add the following to 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 } }
Taking the above code step-by-step:
- Here you retrieve the message based on the NSIndexPath item .
- Check if the message was sent by the local user. If so, return the outgoing image view.
- If the message was not sent by the local user, return the incoming image view.
The final steps before you build and run are to remove avatar support and close the gap where the avatars would normally get displayed.
In ChatViewController add the following method:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! { return nil }
Next, find viewDidLoad() and add the following:
// No avatars collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSizeZero collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero
JSQMessagesViewController does provides support for avatars, but you don’t need (or want) avatars in your anonymous ChatChat app. To remove the avatar image, you simply return nil for each message’s avatar display and tell the layout to size each avatar at CGSizeZero , which is “no size”.
Build and run your app. After logging in, you’ll see a blank chat screen:
Time to start the conversation and add a few messages!
Creating Messages
Create the following method in ChatViewController :
func addMessage(id: String, text: String) { let message = JSQMessage(senderId: id, displayName: "", text: text) messages.append(message) }
This helper method creates a new JSQMessage with a blank displayName and adds it to the data source.
Add a few hardcoded messages in viewDidAppear(_:) to see things in action:
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() }
Build and run you app; you’ll see the messages appear in the conversation view:
Hm, the text is a bit hard to read on the incoming messages. It should probably be black.
Message Bubble Text
As you’ve realized by now, to do almost anything in JSQMessagesViewController , you just need to override a method. To set the text color, use the good old fashioned collectionView(_:cellForItemAtIndexPath:) .
Add the following method in 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 }
If the message is sent by the local user, the text color is white. If it’s not sent by the local user, the text is black.
Build and run; you should see incoming messages in black text:
Boom — that’s one nice looking chat app! Time to make it work (for real) with Firebase.
Firebase Data Structure
Before you get going on the realtime data synchronization, take a moment and think about the data structure first.
The Firebase database is a NoSQL JSON data store. Essentially, everything in the Firebase database is a JSON object, and each key of this JSON object has its own URL.
Here’s a sample of how your data could look as a JSON object:
{ // 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" }, } }
The Firebase database favors a denormalized data structure , so it’s okay to include senderId for each message item. A denormalized data structure means you’ll duplicate a lot of data, but the upside is faster data retrieval. Tradeoffs — we haz them! :]
Setting Up the Firebase Reference
Add the following properties to ChatViewController.swift :
let rootRef = Firebase(url: "https://<my-firebase-app>.firebaseio.com/") var messageRef: Firebase!
In viewDidLoad() , initialize messageRef as follows:
override func viewDidLoad() { super.viewDidLoad() title = "ChatChat" setupBubbles() // No avatars collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSizeZero collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero messageRef = rootRef.childByAppendingPath("messages") }
Creating rootRef creates a connection to the Firebase database. Then to create messageRef , you use childByAppendingPath() , which is simply a helper method for creating a child reference.
In case you’re wondering, creating another reference doesn’t mean you’re creating another connection. Every reference shares the same connection to the same Firebase database.
Sending Messages
You may have been a bit eager and tapped the “Send” button already; if so, you probably saw the app crash. Now that you’re hooked up to the Firebase database, you can send a few messages for real.
First, delete ChatViewController ‘s viewDidAppear(_:) to remove the stub test messages.
Then, override the following method to make the “Send” button save a message to the Firebase database.
override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: NSDate!) { let itemRef = messageRef.childByAutoId() // 1 let messageItem = [ // 2 "text": text, "senderId": senderId ] itemRef.setValue(messageItem) // 3 // 4 JSQSystemSoundPlayer.jsq_playMessageSentSound() // 5 finishSendingMessage() }
Here’s what’s going on:
- Using childByAutoId() , you create a child reference with a unique key.
- Create a dictionary to represent the message. A [String: AnyObject] works as a JSON-like object.
- Save the value at the new child location.
- Play the canonical “message sent” sound.
- Complete the “send” action and reset the input toolbar to empty.
Build and run; open up your Firebase App Dashboard and click on the Data tab. Send a message in the app and you should see the messages appear in the dashboard in real time:
High five! You’re saving messages to the Firebase database like a pro. The messages don’t appear on the screen, but you’ll take care of that next.
Real Time Data Synchronization with Firebase
Every time you update data in the Firebase database, the database pushes the update to every connected app. Data synchronization in Firebase works in three parts: a URL , an event , and a snapshot .
For example, here’s how you can observe for new messages:
let ref = Firebase(url: "https://<my-firebase-app>.firebaseio.com/messages") // 1 ref.observeEventType(.ChildAdded) { (snapshot: FDataSnapshot!) in { // 2 print(snapshot.value) // 3 }
Here’s what’s going on:
- Using your Firebase App URL , you create a Firebase database reference. The URL you pass points to the data to read.
- Call observeEventType(_:FEventType:) , with FEventType.ChildAdded . The child-added event fires off for every item at the URL’s location.
- The closure receives a FDataSnapshot (the snapshot ) that contains the data and other helpful methods.
Synchronizing the Data Source
Now that you’ve seen how easy it is to synchronize data with Firebase, wire up the data source.
Add the following to ChatViewController :
private func observeMessages() { // 1 let messagesQuery = messageRef.queryLimitedToLast(25) // 2 messagesQuery.observeEventType(.ChildAdded) { (snapshot: FDataSnapshot!) in // 3 let id = snapshot.value["senderId"] as! String let text = snapshot.value["text"] as! String // 4 self.addMessage(id, text: text) // 5 self.finishReceivingMessage() } }
Taking each numbered comment in turn:
- Start by creating a query that limits the synchronization to the last 25 messages.
- Use the .ChildAdded event to observe for every child item that has been added, and will be added, at the messages location.
- Extract the senderId and text from snapshot.value .
- Call addMessage() to add the new message to the data source.
- Inform JSQMessagesViewController that a message has been received.
Next, call observeMessages() in viewDidAppear(_:) :
override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) observeMessages() }
Build and run your app; you should see any messages sent earlier along with any new ones you enter:
Congrats! You have a real time chat app! Now it’s time to do some even fancier things, such as detecting when a user is typing.
Knowing When a User is Typing
One of the coolest features of the Messages app is seeing the “user is typing” indicator. When the little bubble pops up, you know another user is typing into the keyboard. This is indicator is super-important, because it keeps us from sending those awkward “Are you still there?” messages.
There are many ways of detecting typing, but textViewDidChange(_:textView:) is a great place to check. For example:
override func textViewDidChange(textView: UITextView) { super.textViewDidChange(textView) // If the text is not empty, the user is typing print(textView.text != "") }
To determine whether the user is typing, you check the value of textView.text . If the value is anything other than the empty string, you know that the user has entered text.
Using Firebase, you can update the Firebase database when a user is typing. Then, in response to the database getting updated with this indication, you can display the “user is typing” indicator.
To do this, first add the following properties to ChatViewController :
var userIsTypingRef: Firebase! // 1 private var localTyping = false // 2 var isTyping: Bool { get { return localTyping } set { // 3 localTyping = newValue userIsTypingRef.setValue(newValue) } }
Here’s what you need to know about these properties:
- Create a reference that tracks whether the local user is typing.
- Store whether the local user is typing in a private property.
- Using a computed property, you can update userIsTypingRef each time you update this property.
Add the following method to ChatViewController :
private func observeTyping() { let typingIndicatorRef = rootRef.childByAppendingPath("typingIndicator") userIsTypingRef = typingIndicatorRef.childByAppendingPath(senderId) userIsTypingRef.onDisconnectRemoveValue() }
This method creates a reference to the URL of /typingIndicator
Update viewDidAppear(_:) to call observeTyping() :
override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) observeMessages() observeTyping() }
Now add textViewDidChange(_:textView:) to ChatViewController and set isTyping for each update:
override func textViewDidChange(textView: UITextView) { super.textViewDidChange(textView) // If the text is not empty, the user is typing isTyping = textView.text != "" }
Finally, add the following code at the end of didPressSendButton(_:withMessageText:senderId:senderDisplayName:date:) function to reset the typing indicator:
isTyping = false
Build and run your app; pull up the Firebase App Dashboard to view the data. When you type a message, you should see the typingIndicator record update for the user:
Wahoo! You now know when a user is typing! Now it’s time to work on displaying the indicator.
Querying for Typing Users
The “user is typing” indicator should display every time any user is typing, with the exception of the local user. You can safely assume that the local user knows when they’re typing.
Using a Firebase query, you can retrieve all of the users that are currently typing. Add the following property to ChatViewController :
var usersTypingQuery: FQuery!
This property holds an FQuery , which is just like a Firebase reference, except that it’s ordered by an order function.
Next, modify observeTyping() like so:
private func observeTyping() { let typingIndicatorRef = rootRef.childByAppendingPath("typingIndicator") userIsTypingRef = typingIndicatorRef.childByAppendingPath(senderId) userIsTypingRef.onDisconnectRemoveValue() // 1 usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqualToValue(true) // 2 usersTypingQuery.observeEventType(.Value) { (data: FDataSnapshot!) in // 3 You're the only typing, don't show the indicator if data.childrenCount == 1 && self.isTyping { return } // 4 Are there others typing? self.showTypingIndicator = data.childrenCount > 0 self.scrollToBottomAnimated(true) } }
Here’s what’s going on:
- You initialize the query by retrieving all users who are typing. This is basically saying, “Hey Firebase, go to the key /typingIndicators and get me all users for whom the value is true .”
- Observe for changes using .Value ; this will give you an update anytime anything changes.
- You need to see how many users are in the query. If the there’s just one user, check to see if the local user is typing. If so, don’t display the indicator.
- If there are more than zero users, and the local user isn’t typing, it’s safe to set the indicator. Call scrollToBottomAnimated(_:animated:) to ensure the indicator is displayed.
Before you build and run, grab a physical iOS device, as testing this situation takes two devices. Use the simulator for one user, and your device for the other.
Now, build and run your app on both the simulator and the device. When one user types you should see the indicator appear:
Kaboom! You just made a big, bad, real time, user-typing-indicating, chat app. Go grab yourself your favorite beverage, you earned it!
Where To Go From Here?
You can download thecompleted project with all of the code you’ve developed in this Firebase tutorial.
You now know the basics of Firebase and JSQMessagesViewController, but there’s plenty more you can do, including one-to-one messaging, social authentication, and avatar display.
To take this app even further, you could take a look at the Firebase iOS documentation .
If you’re interested in authenticating users via social accounts, check out the guide for Firebase user authentication , as it covers 推ter, Google, 非死book, and GitHub.
I hope you’ve enjoyed this Firebase tutorial; if you have any questions feel free to leave them in the (non-anonymous yet avatar-enabled) discussion below! :]
</div>