Image may be NSFW.
Clik here to view.
It seems like every major app out there has a chat feature — and yours should be no different! This Firebase tutorial will show you how.
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 are some great frameworks out there to help you: Firebase lets you synchronize real time data without writing a line of server code, while MessageKit gives you a messaging UI that’s on par with the native Messages app.
In this Firebase tutorial, you’ll build RWRC (Ray Wenderlich Relay Chat) — an anonymous chat application. If you’ve used IRC or Slack, this sort of application should already be familiar to you.
Image may be NSFW.
Clik here to view.
Along the way, you’ll learn how to do the following:
- Set up the Firebase SDK and MessageKit with CocoaPods.
- Synchronize data in real time with the Cloud Firestore.
- Authenticate anonymously with Firebase.
- Leverage MessageKit for a complete chat UI.
- Create multiple message threads.
- Use Firebase Storage to send pictures.
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project. To get you started the project contains a simple dummy login screen, where the credentials are saved to User Defaults.
The starter project has a few helper classes that handle sending data to Firebase and saving data to User Defaults. Feel free to browse the starter project a bit to get familiar with the code.
In the starter project you’ll find ChannelsViewController.swift which listens to changes in a Firebase Firestore database and updates a table view whenever the user adds a new channel. You’ll build a similar implementation for displaying chat messages instead of channels.
You’ll use CocoaPods to install both the Firebase SDK and MessageKit
. 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 and run the following command to install your dependencies:
pod install
This may take a few minutes, but once the packages have installed, open RWRC.xcworkspace in Xcode. Before you can run the app, you’ll need to configure Firebase.
If you’re new to Firebase you’ll need to create an account. Don’t worry — this is easy and totally free.
Create a Firebase Account
Head to the Firebase signup site, create an account, and then create a new Firebase project.
In Xcode, click on the target and change the Bundle Identifier to any value you like, and select a Team in the Signing section.
Follow Steps 1 and 2 of the instructions to add Firebase to an iOS app, starting here:
Image may be NSFW.
Clik here to view.
Next, enter in the app’s bundle ID into the form, after which you will download and add the GoogleService-Info.plist config file to your project under the Supporting Files group as shown in the Firebase instructions. This .plist file contains the configuration information needed for Firebase integration with your app.
Now build and run the app. You should see the following:
Image may be NSFW.
Clik here to view.
Enabling Anonymous Authentication
Firebase lets users log in through email or social accounts, but it can also authenticate users anonymously, giving you a unique identifier for a user without knowing any information about them.
To set up anonymous authentication, open the Firebase App Dashboard, select the Authentication option on the left, click Set Up Sign-In Method, then select the Anonymous option, switch Enable so that it’s on, then click Save.
Image may be NSFW.
Clik here to view.
Just like that, you’ve enabled super secret stealth mode! Okay, so it’s really just anonymous authentication, but hey — it’s still cool. :]
Clik here to view.

Super secret stealth mode achieved!
Logging In
Open LoginViewController.swift and add the following underneath import UIKit
:
import FirebaseAuth
To log in to chat, the app will need to authenticate using the Firebase authentication service. Add the following code to the bottom of signIn:
Auth.auth().signInAnonymously(completion: nil)
That line of code from the FirebaseAuth framework will post the Notification.Name.AuthStateDidChange
notification that AppController
is listening for. Once the notification is fired AppController
will update the root view controller for you.
Build and run your app, enter a display name and tap Get Started.
Image may be NSFW.
Clik here to view.
Once the user signs in, they navigate to the ChannelsViewController
, whose job it is to show the user a list of current channels and allow creating new channels. The table has a single section to display all available channels. There is a toolbar at the bottom with a sign out button, a label displaying your name, and an add button.
Firebase Data Structure
Before you dive into sending messages in realtime, take a moment and think about the data structure first.
Cloud Firestore is a NoSQL JSON data store. Essentially, everything in the Cloud Firestore 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:
{
"channels": [{
"MOuL1sdbrnh0x1zGuXn7": { // channel id
"name": "Puppies",
"thread": [{
"3a6Fo5rrUcBqhUJcLsP0": { // message id
"content": "Wow, that's so cute!",
"created": "May 12, 2018 at 10:44:11 PM UTC-5",
"senderID": "YCrPJF3shzWSHagmr0Zl2WZFBgT2",
"senderName": "naturaln0va",
},
"4LXlVnWnoqyZEuKiiubh": { // message id
"content": "Yes he is.",
"created": "May 12, 2018 at 10:40:05 PM UTC-5",
"senderID": "f84PFeGl2yaqUDaSiTVeqe9gHfD3",
"senderName": "lumberjack16",
},
}]
},
}]
}
Cloud Firestore favors a denormalized data structure, so it’s okay to include senderId
and senderName
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! :]
Chat Interface Setup
MessageKit
is a souped-up UICollectionViewController
that’s customized for chat, so you don’t have to create your own! :]
In this section of the tutorial, you’ll focus on four things:
- Handling input from the message bar.
- Creating message data.
- Styling message bubbles.
- Removing avatar support.
Almost everything you’ll need to do requires that you override methods. MessageKit provides the MessagesDisplayDelegate
, MessagesLayoutDelegate
, and MessagesDataSource
protocols, so you only need to override the default implementations.
MessagesViewController
, check out the full the documentation here.Open ChatViewController.swift and, at the top of ChatViewController
, define the following properties:
private var messages: [Message] = []
private var messageListener: ListenerRegistration?
These properties are similar to those added to the channels view controller. The messages array is the data model and the listener handles clean up.
Now you can start configuring the data source. Above the MessageInputBarDelegate
section, add the following:
// MARK: - MessagesDataSource
extension ChatViewController: MessagesDataSource {
// 1
func currentSender() -> Sender {
return Sender(id: user.uid, displayName: AppSettings.displayName)
}
// 2
func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
// 3
func messageForItem(at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
// 4
func cellTopLabelAttributedText(for message: MessageType,
at indexPath: IndexPath) -> NSAttributedString? {
let name = message.sender.displayName
return NSAttributedString(
string: name,
attributes: [
.font: UIFont.preferredFont(forTextStyle: .caption1),
.foregroundColor: UIColor(white: 0.3, alpha: 1)
]
)
}
}
There’s a bit going on here:
- A sender is a simple
struct
that has anid
andname
property. You create an instance of a sender from the anonymous Firebase user id and the chosen display name. - The number of messages in the collection view will be equal to the local array of messages.
- Your
Message
model object conforms toMessageType
so you can just return the message for the given index path. - The last method returns the attributed text for the name above each message bubble. You can modify the text you’re returning here to your liking, but these are some good defaults.
Build and run the app, add a channel named Cooking and then navigate to it. It should now look like:
Image may be NSFW.
Clik here to view.
So far, so good. Next, you’ll need to implement a few more delegates before you start sending messages.
Setting Up the Display and Layout Delegates
Now that you’ve seen your new awesome chat UI, you probably want to start displaying messages. But before you do that, you have to take care of a few more things.
First, you’ll fine tune some layout parameters from the MessagesLayoutDelegate
. Add the following section below the MessagesDisplayDelegate
section:
// MARK: - MessagesLayoutDelegate
extension ChatViewController: MessagesLayoutDelegate {
func avatarSize(for message: MessageType, at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> CGSize {
// 1
return .zero
}
func footerViewSize(for message: MessageType, at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> CGSize {
// 2
return CGSize(width: 0, height: 8)
}
func heightForLocation(message: MessageType, at indexPath: IndexPath,
with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
// 3
return 0
}
}
Here’s the break down:
- Returning
zero
for the avatar size will hide it from the view. - Adding a little padding on the bottom of each message will help the readability of the chat.
- At the time of writing, MessageKit doesn’t have a default implementation for the height of a location message. Since you won’t be sending a location message in this tutorial, return zero as the default.
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
, replace the MessagesDisplayDelegate
extension with the following:
extension ChatViewController: MessagesDisplayDelegate {
func backgroundColor(for message: MessageType, at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> UIColor {
// 1
return isFromCurrentSender(message: message) ? .primary : .incomingMessage
}
func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> Bool {
// 2
return false
}
func messageStyle(for message: MessageType, at indexPath: IndexPath,
in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
// 3
return .bubbleTail(corner, .curved)
}
}
Taking the above code step-by-step:
- For the given message, you check and see if it’s from the current sender. If it is, you return the app’s primary green color; if not, you return a muted gray color. MessageKit uses this color when creating the background image for the message.
- You return
false
to remove the header from each message. You can use this to display thread specific information, such as a timestamp. - Finally, based on who sent the message, you choose a corner for the tail of the message bubble.
To tie this all together, add the following to the bottom of viewDidLoad()
:
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
Check that your app builds and you can navigate to one of your channels
Image may be NSFW.
Clik here to view.
Believe it or not, that’s all it takes to configure a MessagesViewController
subclass to display messages! Well, it would be more exciting to see some messages, wouldn’t it?
Time to get this conversation started!
Creating Messages
Create the following method below viewDidLoad()
in ChatViewController
:
// MARK: - Helpers
private func insertNewMessage(_ message: Message) {
guard !messages.contains(message) else {
return
}
messages.append(message)
messages.sort()
let isLatestMessage = messages.index(of: message) == (messages.count - 1)
let shouldScrollToBottom = messagesCollectionView.isAtBottom && isLatestMessage
messagesCollectionView.reloadData()
if shouldScrollToBottom {
DispatchQueue.main.async {
self.messagesCollectionView.scrollToBottom(animated: true)
}
}
}
This helper method is similar to the one that’s in ChannelsViewController
. It makes sure the messages array doesn’t already contain the message, then adds it to the collection view. Then, if the new message is the latest and the collection view is at the bottom, scroll to reveal the new message.
Add a test message by overriding viewDidAppear(_:)
:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let testMessage = Message(user: user, content: "I love pizza, what is your favorite kind?")
insertNewMessage(testMessage)
}
Build and run the app; you’ll see your message appear in the conversation view:
Image may be NSFW.
Clik here to view.
Boom — that’s one nice looking chat app! Time to make it work (for real) with Firebase.
Sending Messages
First, delete viewDidAppear(_:)
to remove the test message in ChatViewController
and add the following properties at the top of the file:
private let db = Firestore.firestore()
private var reference: CollectionReference?
At the top of viewDidLoad
add the following:
guard let id = channel.id else {
navigationController?.popViewController(animated: true)
return
}
reference = db.collection(["channels", id, "thread"].joined(separator: "/"))
The reference property is the point in the database where the messages are stored. The id
property on the channel is optional because you might not yet have synced the channel. If the channel doesn’t exist in Firestore yet messages cannot be sent, so returning to the channel list makes the most sense.
Next add the following method to the top of the Helpers
section:
private func save(_ message: Message) {
reference?.addDocument(data: message.representation) { error in
if let e = error {
print("Error sending message: \(e.localizedDescription)")
return
}
self.messagesCollectionView.scrollToBottom()
}
}
This method uses the reference that was just setup. The addDocument
method on the reference takes a dictionary with the keys and values that represent that data. The message data struct implements DatabaseRepresentation
, which defines a dictionary property to fill out.
Open Message.swift and examine the implementation of DatabaseRepresentation
. As you can see, it maps its properties to readable keys and only sets the content of the message if there is no download URL.
Back in ChatViewController.swift, add the following delegate method inside the MessageInputBarDelegate
extension:
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
// 1
let message = Message(user: user, content: text)
// 2
save(message)
// 3
inputBar.inputTextView.text = ""
}
Here’s what’s going on:
- Create a message from the contents of the message bar and the current user.
- Save the message to Cloud Firestore using
save(_:)
. - Clear the message bar’s input field after you send the message.
Build and run; open up your Firebase App Dashboard and click on the Database tab. Select a channel, then send a message in the app and you should see the messages appear in the dashboard in real time:
Image may be NSFW.
Clik here to view.
High five! You’re saving messages to Cloud Firestore like a pro. The messages don’t appear on the screen, but you’ll take care of that next.
Synchronizing the Data Source
Add the following to below insertNewMessage(_:)
in ChatViewController
:
private func handleDocumentChange(_ change: DocumentChange) {
guard let message = Message(document: change.document) else {
return
}
switch change.type {
case .added:
insertNewMessage(message)
default:
break
}
}
This is very similar to how ChannelsViewController
observes new database changes. For brevity, the only change type you handle in the switch statement is add
.
Next, add the following code below the reference initialization in viewDidLoad()
:
messageListener = reference?.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { change in
self.handleDocumentChange(change)
}
}
Firestore
calls this snapshot listener whenever there is a change to the database.
To clean things up add a deinit
towards the top of the file:
deinit {
messageListener?.remove()
}
Build and run your app; you should see any messages sent earlier along with any new ones you enter:
Image may be NSFW.
Clik here to view.
Congrats! You have a real time chat app! Now it’s time to add one final finishing touch.
Sending Images
To send images, you’re going to follow mostly the same principle as sending text with one key difference. Rather than storing the image data directly with the message, you’ll use Firebase Storage, which is better suited to storing large files like audio, video or images.
To start, you need to add the Photos import to ChatViewController.swift:
import Photos
Add the following above the Helpers section:
// MARK: - Actions
@objc private func cameraButtonPressed() {
let picker = UIImagePickerController()
picker.delegate = self
if UIImagePickerController.isSourceTypeAvailable(.camera) {
picker.sourceType = .camera
} else {
picker.sourceType = .photoLibrary
}
present(picker, animated: true, completion: nil)
}
This method will present an image picker controller to allow the user to select an image.
Next, add the following to viewDidLoad()
:
// 1
let cameraItem = InputBarButtonItem(type: .system)
cameraItem.tintColor = .primary
cameraItem.image = #imageLiteral(resourceName: "camera")
// 2
cameraItem.addTarget(
self,
action: #selector(cameraButtonPressed),
for: .primaryActionTriggered
)
cameraItem.setSize(CGSize(width: 60, height: 30), animated: false)
messageInputBar.leftStackView.alignment = .center
messageInputBar.setLeftStackViewWidthConstant(to: 50, animated: false)
// 3
messageInputBar.setStackViewItems([cameraItem], forStack: .left, animated: false)
Going through this:
- Create a new
InputBarButtonItem
with a tint color and an image. - Connect the new button to
cameraButtonPressed()
. - Lastly, add the item to the left side of the message bar.
Sending a photo message is a little different then sending a plain text message. Saving a photo to Firebase Storage
returns a URL, but this may take a couple of seconds — perhaps longer, if the network connection is poor. Rather than blocking the user interface during this time, which will make your app feel slow, you’ll start sending the message and disable the camera message bar item.
Add the following properties at the top of ChatViewController
:
private var isSendingPhoto = false {
didSet {
DispatchQueue.main.async {
self.messageInputBar.leftStackViewItems.forEach { item in
item.isEnabled = !self.isSendingPhoto
}
}
}
}
private let storage = Storage.storage().reference()
and add this method to the bottom of the Helpers section:
private func uploadImage(_ image: UIImage, to channel: Channel, completion: @escaping (URL?) -> Void) {
guard let channelID = channel.id else {
completion(nil)
return
}
guard let scaledImage = image.scaledToSafeUploadSize,
let data = scaledImage.jpegData(compressionQuality: 0.4) else {
completion(nil)
return
}
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
let imageName = [UUID().uuidString, String(Date().timeIntervalSince1970)].joined()
storage.child(channelID).child(imageName).putData(data, metadata: metadata) { meta, error in
completion(meta?.downloadURL())
}
}
The isSendingPhoto
property takes care of updating the camera item when it changes and the storage
property is a reference to the root of Firebase Storage. uploadImage(_:to:completion:)
uploads an image to the specified channel in the Firebase Storage.
Below uploadImage(_:to:completion:)
, add:
private func sendPhoto(_ image: UIImage) {
isSendingPhoto = true
uploadImage(image, to: channel) { [weak self] url in
guard let `self` = self else {
return
}
self.isSendingPhoto = false
guard let url = url else {
return
}
var message = Message(user: self.user, image: image)
message.downloadURL = url
self.save(message)
self.messagesCollectionView.scrollToBottom()
}
}
This method takes care of updating the isSendingPhoto
property to update the UI. Once the photo upload completes and the URL to that photo is returned, save a new message with that photo URL to the database.
Next, to use sendPhoto(_:)
, add the following image picker delegate methods to the UIImagePickerControllerDelegate
extension:
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true, completion: nil)
// 1
if let asset = info[.phAsset] as? PHAsset {
let size = CGSize(width: 500, height: 500)
PHImageManager.default().requestImage(
for: asset,
targetSize: size,
contentMode: .aspectFit,
options: nil) { result, info in
guard let image = result else {
return
}
self.sendPhoto(image)
}
// 2
} else if let image = info[.originalImage] as? UIImage {
sendPhoto(image)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
These two methods handle the cases when the user either selects an image or cancels the selection process. When selecting an image, the user can either get one from the photo library or take an image directly with the camera.
Here’s what this does:
- If the user selected an asset, the selected image needs to be downloaded from iCloud. Request it at a fixed size. Once it’s successfully retrieved, send it.
- If there is an original image in the info dictionary, send that. You don’t need to worry about the original image being too large here because the storage helper handles resizing the image for you. Have a look at UIImage+Additions.swift to see how the resizing is done.
Nearly there! You’ve now set up your app to save the image data to Firebase Storage and save the URL to the message data, but you’ve not yet updated the app to display those photos. Time to fix that.
Get started by adding the following to the bottom of the Helpers
section:
private func downloadImage(at url: URL, completion: @escaping (UIImage?) -> Void) {
let ref = Storage.storage().reference(forURL: url.absoluteString)
let megaByte = Int64(1 * 1024 * 1024)
ref.getData(maxSize: megaByte) { data, error in
guard let imageData = data else {
completion(nil)
return
}
completion(UIImage(data: imageData))
}
}
This method asynchronously downloads an image at the specified path from Firebase Storage.
Next, change the guard
statement from a constant to a variable in the handleDocumentChange(_:)
method:
guard var message = Message(document: change.document) else {
return
}
Then, in handleDocumentChange(_:)
, replace the content of the .added
case with the following:
if let url = message.downloadURL {
downloadImage(at: url) { [weak self] image in
guard let self = self else {
return
}
guard let image = image else {
return
}
message.image = image
self.insertNewMessage(message)
}
} else {
insertNewMessage(message)
}
Build and run the app; tap on the little camera icon and send a photo message in your chat. Notice how the camera icon is disabled when your app is saving the photo data to Firebase Storage.
Image may be NSFW.
Clik here to view.
Kaboom! You just made a big, bad, real time, photo and text sending chat app. Go grab yourself your favorite beverage, you earned it!
Where to Go From Here?
Use the Download Materials button at the top or bottom of this tutorial to download the completed project.
You now know the basics of Cloud Firestore and MessageKit, 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. You can also take a look at our 22 part video course on Beginning Firebase!
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! :]
The post Firebase Tutorial: Real-time Chat appeared first on Ray Wenderlich.