Firebase Tutorial: Real-Time Chat
Learn to build a chat app with Firebase and MessageKit! By Yusuf Tör.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Firebase Tutorial: Real-Time Chat
30 mins
- Getting Started
- Creating a Firebase Account
- Enabling Anonymous Authentication
- Logging In
- Choosing a Firebase Database
- Firebase Data Structure
- Setting Up the Chat Interface
- Setting Up the Display and Layout Delegates
- Creating Messages
- Sending Messages
- Creating the Database
- Synchronizing the Data Source
- Sending Images
- Displaying Photos in Threads
- Where to Go From Here?
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:
- Handling input from the input bar
- Creating message data
- Styling message bubbles
- 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.
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:
- Each message takes up a section in the collection view.
- 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 ofSender
. - Your
Message
model object conforms toMessageType
so you 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. Add a channel named Cooking and then navigate to it. It’ll look like this:
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:
- Adds a little bit of padding on the bottom of each message to improve the chat’s readability.
- 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:
- 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.
- You return
false
to remove the header from each message. You could use this to display thread-specific information, such as a timestamp. - Then you hide the avatar from the view as that is not necessary in this app.
- 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.
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!