Firebase Tutorial: Real-Time Chat

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

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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() {
  guard 
    let layout = messagesCollectionView.collectionViewLayout
      as? MessagesCollectionViewFlowLayout
  else {
    return
  }
  layout.textMessageSizeCalculator.outgoingAvatarSize = .zero
  layout.textMessageSizeCalculator.incomingAvatarSize = .zero
  layout.setMessageIncomingAvatarSize(.zero)
  layout.setMessageOutgoingAvatarSize(.zero)
  let incomingLabelAlignment = LabelAlignment(
    textAlignment: .left,
    textInsets: UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0))
  layout.setMessageIncomingMessageTopLabelAlignment(incomingLabelAlignment)
  let outgoingLabelAlignment = LabelAlignment(
    textAlignment: .right,
    textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15))
  layout.setMessageOutgoingMessageTopLabelAlignment(outgoingLabelAlignment)
}

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():

removeMessageAvatars()

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!