Firebase Tutorial: Real-Time Chat

Learn to build a chat app with Firebase and MessageKit! By Yusuf Tör.

Login to leave a rating/review
Download materials
Save for later

Learn to build a chat app with Firebase and MessageKit!

Update note: Yusuf Tör updated this tutorial for iOS 14, Swift 5, Xcode 12 and MessageKit 3.6. Tom Elliott wrote the original.

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 to add real-time chat to your app.

However, creating a chat tool can seem like a daunting task. There are no native UIKit controls specifically designed for chat, and you need a server to coordinate and store the 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.
  • MessageKit gives you a messaging UI that’s on par with the native Messages app.

In this tutorial, you’ll build RWRC, or Ray Wenderlich Relay Chat, an anonymous chat app. If you’ve used IRC or Slack, you’re already familiar with this sort of app.

A conversation thread about puppies

Along the way, you’ll learn how to:

  • Authenticate anonymously with Firebase.
  • Create multiple chat channels.
  • Leverage MessageKit for a complete chat UI.
  • Synchronize data in real-time with the Firestore database.
  • Use Firebase Storage to send pictures.

Getting Started

Click Download Materials at the top or bottom of this tutorial to download the starter project.

Open the starter project and take a quick look around. The starter project contains a simple dummy login screen that saves the credentials to UserDefaults. It also has a few helper classes for sending data to Firebase and saving data to UserDefaults.

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 to display chat messages instead of channels.

You’ll find the Firebase SDK and MessageKit are already in the project as Swift Packages. These will automatically install when you open the project.

Note: If you’re new to the Swift Package Manager, check out our Introduction to Swift Package Manager tutorial for an in depth explanation.

Before you can run the app, you’ll need to configure Firebase.

Creating a Firebase Account

If you’re new to Firebase, you’ll need to create an account. Don’t worry! It’s easy and free.

Head to the Firebase signup site and create an account. Then create and name a new Firebase project called RWRC. Make sure that you disable support for Google Analytics as it won’t be necessary for this tutorial.

Note: For a detailed walk-through on setting up Firebase, see the Getting Started with Firebase Tutorial.

In Xcode, click the target and change the Bundle Identifier to any value you like. Then select a Team in the Signing section.

In the Project Overview in Firebase, click iOS. You’ll see instructions to add Firebase to your iOS app:

Add Firebase to iOS app

Next, enter the app’s bundle ID (the one you chose earlier in Xcode) and name (RWRC) into the form and click Register app:

Register bundle ID and app name with Firebase

Download and add GoogleService-Info.plist to your project under the Supporting Files group as shown in the Firebase instructions. This file contains the configuration information you need to integrate Firebase with your app:

Download config file

Note: Do only steps one and two of the instructions. The rest is already done in the starter project and your app will crash if you duplicate the steps.

Now build and run. You’ll see the following:

Login screen

That’s a good start, but right now the application login screen doesn’t actually do anything. You’ll now hook that up to Firebase.

Enabling Anonymous Authentication

Firebase lets users log in through email or social accounts. However, it can also authenticate users anonymously, giving them unique identifiers without knowing their personally identifiable information.

To set up anonymous authentication, open the Firebase console for the app you made earlier. Select Authentication on the left and click Get started:

Firebase authentication console

Then select Anonymous. Toggle Enable and click Save:

Enabling anonymous authentication

Just like that, you enabled super secret stealth mode! Okay, so it’s just anonymous authentication. But hey, it’s still cool. :]

Stealth Swift

It’s now time to set up the login within the app itself.

Logging In

Open LoginViewController.swift. Under import UIKit, add:

import FirebaseAuth

To log in to chat, the app will need to authenticate using the Firebase authentication service. Add the following code to the end of signIn():


This method asynchronously logs into Firebase anonymously. If the device has already signed in, then the existing user is signed in, otherwise a new user is created. Once the sign in has completed Firebase posts the AuthStateDidChange notification that AppController is listening for. AppController updates the root view controller for you when the notification fires.

Build and run. Enter a display name and tap Get Started:

Empty channel list

Once the user signs in, they automatically navigate to the ChannelsViewController. They’ll see a list of current channels and have the option to create new channels. The table has a single section to display all available channels.

At the bottom, they’ll see a toolbar with a sign-out button, a label displaying their name and an add button.

Before you dive into sending messages in real-time, take a minute to learn about the databases Firebase has to offer.

Choosing a Firebase Database

Firebase comes with two NoSQL JSON databases: Firestore and Realtime Database.

Initially, Firebase only had Realtime Database, an efficient, low-latency database that stores data in one big JSON tree.

However, this wasn’t the best solution for all use cases. So the Firebase team improved on the success of Realtime Database with a new, more intuitive data model called Firestore.

Firestore stores data as documents that contain a set of key-value pairs. It organizes these documents into collections. Each document can have sub-collections.

Each database has strengths and weaknesses.

The Realtime Database:


  • Supports user presence, so you can tell when a user is online or offline.
  • Has extremely low latency.
  • Charges for bandwidth and storage but not for operations performed in the database.
  • Scales to 200k concurrent connections.


  • Has no multi-region support. Data is available in regional configurations only.
  • Has limited sorting and filtering functionality.

The Firestore database:


  • More structured than the Realtime Database and can perform more complex queries on the data.
  • Designed to scale better than the Realtime Database. The scaling limit is currently around one million concurrent connections.
  • Has multiple data centers storing data in distinct regions and can support multi-regional configurations.
  • Charges primarily on operations performed in the database and, at a lower rate, bandwidth and storage.


  • Doesn’t allow documents to update at a rate greater than once per second.
  • Doesn’t support user presence.

For this tutorial, you’ll use Firestore as your database. However, in a production chat app with lots of reads and writes to the database, you may choose the Realtime Database to reduce costs.

You can also use both the Firestore and Realtime Database within your app. For more information about these databases, take a look at Firebase’s documentation.

Now that you know a little about the Firebase database, it is time to learn about the structure of the data you’ll store in the database.

Firebase Data Structure

You learned that Firestore is a NoSQL JSON data store, but what is that exactly? Essentially, everything in 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": "April 12, 2021 at 10:44:11 PM UTC-5",
          "senderId": "YCrPJF3shzWSHagmr0Zl2WZFBgT2",
          "senderName": "naturaln0va",
        "4LXlVnWnoqyZEuKiiubh": { // message id
          "content": "Yes he is.",
          "created": "April 12, 2021 at 10:40:05 PM UTC-5",
          "senderId": "f84PFeGl2yaqUDaSiTVeqe9gHfD3",
          "senderName": "lumberjack16",

You can see here there is a main JSON object with a single key called channels. The channels value is an array of objects. These channel objects are keyed by an identifier and contain a name & a thread. The thread is an array of objects, each of which is a single message containing the message in the content field, a created date and the identifier and name of the message’s sender.

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.

That data structure looks good so it’s time to crack on with setting up the app to handle those chat threads!

Setting Up the Chat Interface

MessageKit is a souped-up UICollectionViewController customized for chat, so you don’t have to create your own! :]

In this section of the tutorial, you’ll focus on four things:

  1. Handling input from the input bar
  2. Creating message data
  3. Styling message bubbles
  4. Removing avatar support

Almost everything you need to do requires you to override methods. MessageKit provides the MessagesDisplayDelegate, MessagesLayoutDelegate and MessagesDataSource protocols, so you only need to override the default implementations.

Note: For more information on customizing and working with MessagesViewController, check out the full documentation.

Open ChatViewController.swift. At the top of ChatViewController, define the following properties:

private var messages: [Message] = []
private var messageListener: ListenerRegistration?

The messages array is the data model, and the messageListener is a listener which handles clean up.

Now you can start configuring the data source. Above the InputBarAccessoryViewDelegate section, add:

// MARK: - MessagesDataSource
extension ChatViewController: MessagesDataSource {
  // 1
  func numberOfSections(
    in messagesCollectionView: MessagesCollectionView
  ) -> Int {
    return messages.count

  // 2
  func currentSender() -> SenderType {
    return Sender(senderId: user.uid, displayName: AppSettings.displayName)

  // 3
  func messageForItem(
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> MessageType {
    return messages[indexPath.section]

  // 4
  func messageTopLabelAttributedText(
    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)

This implements the MessagesDataSource protocol from MessageKit. There’s a bit going on here:

  1. Each message takes up a section in the collection view.
  2. MessageKit needs to know name and ID for the logged in user. You tell it that by giving it something conforming to SenderType. In your case, it’s an instance of Sender.
  3. Your Message model object conforms to MessageType so you return the message for the given index path.
  4. 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. Add a channel named Cooking and then navigate to it. It’ll look like this:

Empty message thread

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.

Still in ChatViewController.swift, add the following section below the MessagesDisplayDelegate section:

// MARK: - MessagesLayoutDelegate
extension ChatViewController: MessagesLayoutDelegate {
  // 1
  func footerViewSize(
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> CGSize {
    return CGSize(width: 0, height: 8)

  // 2
  func messageTopLabelHeight(
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> CGFloat {
    return 20

This code:

  1. Adds a little bit of padding on the bottom of each message to improve the chat’s readability.
  2. Sets the height of the top label above each message. This label will hold the sender’s name.

The messages displayed in the collection view are simply images with text overlaid. There are two types of messages: outgoing and incoming. Outgoing messages display on the right and incoming messages on the left.

In ChatViewController, replace MessagesDisplayDelegate with:

// MARK: - MessagesDisplayDelegate
extension ChatViewController: MessagesDisplayDelegate {
  // 1
  func backgroundColor(
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> UIColor {
    return isFromCurrentSender(message: message) ? .primary : .incomingMessage

  // 2
  func shouldDisplayHeader(
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> Bool {
    return false

  // 3
  func configureAvatarView(
    _ avatarView: AvatarView,
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) {
    avatarView.isHidden = true

  // 4
  func messageStyle(
    for message: MessageType,
    at indexPath: IndexPath,
    in messagesCollectionView: MessagesCollectionView
  ) -> MessageStyle {
    let corner: MessageStyle.TailCorner = 
      isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
    return .bubbleTail(corner, .curved)

Taking the code above step-by-step:

  1. For a given message, you check to 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 for the background image of the message.
  2. You return false to remove the header from each message. You could use this to display thread-specific information, such as a timestamp.
  3. Then you hide the avatar from the view as that is not necessary in this app.
  4. Finally, based on who sent the message, you choose a corner for the tail of the message bubble.

Although the avatar is no longer visible, it still leaves a blank space in its place.

Below setUpMessageView() add:

private func removeMessageAvatars() {
    let layout = messagesCollectionView.collectionViewLayout
      as? MessagesCollectionViewFlowLayout
  else {
  layout.textMessageSizeCalculator.outgoingAvatarSize = .zero
  layout.textMessageSizeCalculator.incomingAvatarSize = .zero
  let incomingLabelAlignment = LabelAlignment(
    textAlignment: .left,
    textInsets: UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0))
  let outgoingLabelAlignment = LabelAlignment(
    textAlignment: .right,
    textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15))

This code removes the blank space left for each hidden avatar and adjusts the inset of the top label above each message.

Next, add the following to the bottom of viewDidLoad():


Finally, set the relevant delegates. Add the following to the bottom of setUpMessageView():

messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self

Build and run. Verify that you can navigate to one of your channels.

Another empty message thread

Believe it or not, that’s all it takes to configure a MessagesViewController to display messages!

Well, it would be more exciting to see some messages, wouldn’t it? Time to get this conversation started!

Creating Messages

In ChatViewController, below viewDidLoad(), add:

// MARK: - Helpers
private func insertNewMessage(_ message: Message) {
  if messages.contains(message) {


  let isLatestMessage = messages.firstIndex(of: message) == (messages.count - 1)
  let shouldScrollToBottom =
    messagesCollectionView.isAtBottom && isLatestMessage


  if shouldScrollToBottom {
    messagesCollectionView.scrollToLastItem(animated: true)

This method adds a new message. It first makes sure the message isn’t already present, then adds it to the collection view. Then, if the new message is the latest and the collection view is at the bottom, it scrolls to reveal the new message.

Now add the following underneath viewDidLoad():

override func viewDidAppear(_ animated: Bool) {
  let testMessage = Message(
    user: user,
    content: "I love pizza; what is your favorite kind?")

This adds a simple test message when the view appears.

Build and run. You’ll see your message appear in the conversation view:

A test message

Boom! That’s one good-looking chat app! Time to make it work for real with Firebase.

Sending Messages

First, delete viewDidAppear(_:) to remove the test message in ChatViewController. Then, add the following properties at the top of the class:

private let database = Firestore.firestore()
private var reference: CollectionReference?

Below viewDidLoad(), add:

private func listenToMessages() {
  guard let id = else {
    navigationController?.popViewController(animated: true)

  reference = database.collection("channels/\(id)/thread")

First the id on the channel is checked for nil because you might not have synced the channel yet. It shouldn’t be possible to send messages if the channel doesn’t exist in Firestore yet, so returning to the channel list makes the most sense. Then the reference is set up to reference the thread array on the channel in the database.

Then, in viewDidLoad(), add the following below super.viewDidLoad():


Next, add the following method to the top of the Helpers section:

private func save(_ message: Message) {
  reference?.addDocument(data: message.representation) { [weak self] error in
    guard let self = self else { return }
    if let error = error {
      print("Error sending message: \(error.localizedDescription)")

This method uses the reference you just set up. addDocument on the reference takes a dictionary with the keys and values representing that data. The message data structure implements DatabaseRepresentation, which defines a dictionary property to fill out.

Back in ChatViewController.swift, add the following delegate method inside InputBarAccessoryViewDelegate:

func inputBar(
  _ inputBar: InputBarAccessoryView,
  didPressSendButtonWith text: String
) {
  // 1
  let message = Message(user: user, content: text)

  // 2

  // 3
  inputBar.inputTextView.text = ""

Here you:

  1. Create a Message from the contents of the input bar and the current user.
  2. Save the message to the Firestore database.
  3. Clear the input bar’s text view after you send the message, ready for the user to send the next message.

Next, you need to create a database.

Creating the Database

Open your app’s Firebase console. Click Firestore Database on the left and Create database:

Creating a Firestore database

When you create a database for a real-world setup, you’ll want to configure security rules but they’re not necessary for this tutorial. Select Start in test mode and click Next. You can read more about security rules in the Firestore documentation.

Configuring security rules for Firestore

You can configure Firestore to store data in different regions across the world. For now, leave the location as the default setting and click Enable to create your database:

Setting the Firestore database location

Build and run. Select a channel and send a message.

You’ll see the messages appear in the dashboard in real-time. You may need to refresh the page if you don’t see any updates when you add the first message:

Saving a message to the database

High five! You’re saving messages to Firestore like a pro. The messages don’t appear on the screen, but you’ll take care of that next.

Synchronizing the Data Source

In ChatViewController, add the following below insertNewMessage(_:):

private func handleDocumentChange(_ change: DocumentChange) {
  guard let message = Message(document: change.document) else {

  switch change.type {
  case .added:

For simplicity in this tutorial, the only change type you handle in the switch statement is added.

Next, add the following code to the bottom of listenToMessages():

messageListener = reference?
  .addSnapshotListener { [weak self] querySnapshot, error in
    guard let self = self else { return }
    guard let snapshot = querySnapshot else {
        Error listening for channel updates: \
        \(error?.localizedDescription ?? "No error")

    snapshot.documentChanges.forEach { change in

Firestore calls this snapshot listener whenever there’s a change to the database.

You need to clean up that listener. So add this above viewDidLoad():

deinit {

Build and run. You’ll see any messages sent earlier along with any new ones you enter:

Messages showing in the channel

Congrats! You have a real-time chat app! Now it’s time to add one final finishing touch.

Sending Images

To send images, you’ll 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.

Add the following at the top of ChatViewController.swift:

import Photos

Then above the Helpers section add:

// 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)

This method presents an image picker controller to let the user select an image.

Then, add the following code below removeMessageAvatars():

private func addCameraBarButton() {
  // 1
  let cameraItem = InputBarButtonItem(type: .system)
  cameraItem.tintColor = .primary
  cameraItem.image = UIImage(named: "camera")

  // 2
    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
    .setStackViewItems([cameraItem], forStack: .left, animated: false)

Here you:

  1. Create a new InputBarButtonItem with a tint color and an image.
  2. Connect the new button to cameraButtonPressed().
  3. Add the item to the left side of the message bar.

Next, add the following to the bottom of viewDidLoad():


Sending a photo message is a little different than sending a plain text message. Uploading a photo to Firebase Storage 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.

At the top of ChatViewController add:

private var isSendingPhoto = false {
  didSet {
    messageInputBar.leftStackViewItems.forEach { item in
      guard let item = item as? InputBarButtonItem else {
      item.isEnabled = !self.isSendingPhoto

private let storage =

isSendingPhoto updates the camera button to be enabled only when there is no photo sending in progress. storage is a reference to the root of Firebase Storage.

Then, add this method to the bottom of the Helpers section:

private func uploadImage(
  _ image: UIImage,
  to channel: Channel,
  completion: @escaping (URL?) -> Void
) {
    let channelId =,
    let scaledImage = image.scaledToSafeUploadSize,
    let data = scaledImage.jpegData(compressionQuality: 0.4)
  else {
    return completion(nil)

  let metadata = StorageMetadata()
  metadata.contentType = "image/jpeg"

  let imageName = [UUID().uuidString, String(Date().timeIntervalSince1970)]
  let imageReference = storage.child("\(channelId)/\(imageName)")
  imageReference.putData(data, metadata: metadata) { _, _ in
    imageReference.downloadURL { url, _ in

This method 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 {

    var message = Message(user: self.user, image: image)
    message.downloadURL = url

This method first updates isSendingPhoto to update the UI. Then it kicks off the upload and once the photo upload completes and returns the URL to that photo, it saves a new message with that photo URL to the database.

Before you can use sendPhoto(_:), you need to add some image picker delegate methods. To UIImagePickerControllerDelegate add:

func imagePickerController(
  _ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
  picker.dismiss(animated: true)

  // 1
  if let asset = info[.phAsset] as? PHAsset {
    let size = CGSize(width: 500, height: 500)
      for: asset,
      targetSize: size,
      contentMode: .aspectFit,
      options: nil
    ) { result, _ in
      guard let image = result else {

  // 2
  } else if let image = info[.originalImage] as? UIImage {

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  picker.dismiss(animated: true)

These two methods handle cases when the user either selects an image or cancels the selection process. When choosing an image, the user can either get one from the photo library or take an image directly with the camera.

Here’s a breakdown:

  1. If the user selected an asset, you request to download it from the user’s photo library at a fixed size. Once it’s successfully retrieved, you send it.
  2. If there’s an original image in the info dictionary, send that. You don’t need to worry about the original image being too large because the storage helper handles resizing the image for you. Look at UIImage+Additions.swift for the resizing implementation.

Nearly there! You’ve set up your app to save the image data to Firebase Storage and save the URL to the message data. But you haven’t updated the app to display those photos yet.

Time to fix that.

Displaying Photos in Threads

To the bottom of the Helpers section, add:

private func downloadImage(
  at url: URL,
  completion: @escaping (UIImage?) -> Void
) {
  let ref = url.absoluteString)
  let megaByte = Int64(1 * 1024 * 1024)

  ref.getData(maxSize: megaByte) { data, _ in
    guard let imageData = data else {
    completion(UIImage(data: imageData))

This method asynchronously downloads an image at the specified path from Firebase Storage.

Next find handleDocumentChange(_:) and change the variable in the guard from a constant to a variable:

guard var message = Message(document: change.document) else {

Then, in handleDocumentChange(_:), replace the content of the .added case with:

if let url = message.downloadURL {
  downloadImage(at: url) { [weak self] image in
      let self = self,
      let image = image
    else {
    message.image = image
} else {

Now build and run the app. Tap the little camera icon and send a photo message in your chat. Notice how the camera icon isn’t enabled when your app saves the photo data to Firebase Storage.

Sending a photo message to the channel

Kaboom! You made a big, bad, real-time photo and text sending chat app.

Grab your favorite beverage. You earned it!

Where to Go From Here?

Click Download Materials at the top or bottom of this tutorial to download the completed project.

You now know the basics of 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, 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! :]