July 7, 2020

CometChat tutorial: Add Read Receipts to Your iOS Chat App

Marin Benčević

Prerequisites

Introduction

One of the key differences between text chat and face-to-face conversation is the lack of all non-verbal communication. We've all been there – mistaking a friend's banal sentence as an insult, miscommunication of our intentions, or seeming unfriendly to our customers. There are lots of ways you can increase transparency and provide additional non-verbal communication between two users of a chat app. Reactions, stickers, emojis and even read receipts are all examples of providing additional information to make your users feel more at home.


In this tutorial, we'll focus on adding read and delivery receipts to your iOS app. Read and delivery receipts have been popularized by WhatsApp, and are now standard in almost all big chat apps, whether it's Facebook Messenger, iMessage, Viber or other apps. Adding read receipts lets your users know when they can expect a response. Knowing someone is on the other end of your messages gives you a feeling of real-time communication that you can't get otherwise, leading to a better, more engaging chat experience.


Delivery receipts are great when users are first starting out using your app. Using a new chat UI can be intimidating, and giving your user feedback that their message was delivered will give them a sense of confidence that your app works correctly.



Thankfully, CometChat already has built-in read and delivery receipts, and all you need to do is to hook into the existing mechanisms. By the end of the tutorial, you'll have a chat app that shows a checkmark and a text saying "Delivered" when a message gets delivered, and a double-checkmark and a text saying "Sent" when the message gets sent.


    Note: You can see the full implementation on GitHub.

Setting up

Before you get started, there's a couple of prerequisites to go over. This tutorial assumes you have a working iOS chat app built with CometChat. If you don't, don't worry! We have lots of tutorials to get you set up quickly:



Getting set up with the starter project


Alternatively, you can get started by heading to the start-here branch of this tutorial's GitHub repo and downloading or cloning the contents. The starter project already includes some UI changes for you – the incoming and outgoing message cells already have a stack view that shows the checkmark and text for the read and delivery receipts.


Once you've downloaded the starter project, navigate to the CometChat/ folder in Terminal. You'll use CocoaPods to install all the necessary dependencies:


pod install


    Note: If you don't have CocoaPods installed, take a look at their installation instructions.


Setting up a CometChat app


    Note: this section is optional – if you already have a CometChat app skip ahead to Adding your App ID.


Next, you'll need a CometChat account and a CometChat app. If you don't already have a CometChat account, don't fret. It's free and only takes a couple of minutes to get started.

Head over to CometChat's registration page and sign up. Once you sign up, you'll get taken to a dashboard. Create a new app under Add New App. Select US as the region and enter CometChat as the app's name (or whichever name you like). Click the Add App to create the app.



Adding your App ID


From the CometChat dashboard, click on your app.



Copy the Auth Key (API key) and the APP ID to the appropriate constants in ChatService.swift in Xcode.


private enum Constants {

   #warning("Don't forget to set your API key and app ID here!")

   static let cometChatAPIKey = "API_KEY"

   static let cometChatAppID = "APP_ID"

}


This will connect your CometChat app with the starter project. You can run the project now to get familiar with the app. You can use the emails superhero1 trough superhero5 to log in. These are special test usernames that CometChat sets up for you.


Now that you have everything set up, you can get started working on the read receipts!

The UI

The starter project already includes some UI changes for you. Most notably, IncomingMessageTableViewCell and its outgoing equivalent both include IBOutlets to a stack view (deliveryStatusStackView) showing the read receipt label (deliveryStatusLabel) and the read receipt icon (deliveryStatusIcon).


During this iOS read receipts tutorial, you'll modify these outlets with the correct values for the delivery status of the message.


Modifying the Message struct


To add delivery status to your app, you'll first need to add this status to the Message struct. Open Message.swift and declare a new enum inside the struct:


struct Message {

 enum DeliveryStatus {

   case unknown, delivered, read

 }  ...

}


This enum tells you whether the message was delivered, read or if it's still being sent.

Next, add the following property to the struct:


var deliveryStatus: DeliveryStatus = .unknown


Now each Message has an attached delivery status.


Message already has an initializer that converts CometChat's TextMessage into a Message. TextMessage also holds information you can use to track the delivery status of the message. Add the following code to the bottom of the init in the extension:


if textMessage.deliveredAt > 0 {

 deliveryStatus = .delivered

}

if textMessage.readAt > 0 {

 deliveryStatus = .read

}


CometChat's TextMessage has two properties that change based on whether the message was delivered or read: deliveredAt and readAt. If the message was delivered, deliveredAt will contain a timestamp of when the delivery occurred. Likewise, readAt will be the timestamp of when the user has read the message. If one of these events didn't occur, though, the timestamp will be zero. That's why you check if deliveredAt or readAt is larger than zero, and if they are, you can assume the message was delivered or read, respectively.


Now that your Message has information about the delivery status, you can use that status to update the UI.


Modifying the UI


You'll start by populating the outlets mentioned earlier inside IncomingMessageTableViewCell.swift.


Open the file and add the following code to the end of message's didSet block:


// 1

var deliveryText: String?

var deliveryIcon: UIImage?

switch message.deliveryStatus {

case .delivered:

 // 2

 deliveryIcon = #imageLiteral(resourceName: "checkmark_single")

   .withRenderingMode(.alwaysTemplate)

 deliveryText = "Delivered"

case .read:

 // 3

 deliveryIcon = #imageLiteral(resourceName: "checkmark_double")

   .withRenderingMode(.alwaysTemplate)

 deliveryText = "Read"

case .unknown:

 break

}

// 4

deliveryStatusStackView.isHidden = deliveryText == nil

// 5

deliveryStatusLabel.text = deliveryText

deliveryStatusIcon.image = deliveryIcon


Here's what's happening in the code above:


  1. You declare a text and an icon that you'll use to present the delivery status of the message to the user.
  2. If the status is .delivered, you'll assign the text to "Delivered" and use a single checkmark image.
  3. If the status is .read, you'll assign the text to "Read" and use a double checkmark.
  4. If the status wasn't one of the two mentioned, you'll hide the stack view entirely.
  5. Finally, you set the icon and the text to their respective outlets.


Next, add the following property to the class:


var showsDeliveryStatus: Bool = true {

 didSet {

   deliveryStatusStackView.isHidden = !showsDeliveryStatus

 }

}


This property will let the table view data source determine when the cell shows the delivery status. You'll use this in a bit.


Next, repeat all the steps from the start of this section for OutgoingMessageTableViewCell.swift: Add the code to the didSet and then add showsDeliveryStatus to the class, just like you did above.


Now that both of your classes know how to show delivery status, you can add their common showsDeliveryStatus property as part of the MessageTableViewCell protocol. Open MessageTableViewCell.swift and add a new property to the protocol:


var showsDeliveryStatus: Bool { get set }


Now, you can use showsDeliveryStatus to toggle showing the status on each cell. Open ChatViewController.swift and add the following line to the bottom of tableView( _:cellForRowAt:), right before the return statement:


// Show delivery status if this is the last cell and its outgoing

cell.showsDeliveryStatus = indexPath.row == messages.count - 1 && !message.isIncoming


This line will set showsDeliveryStatus to true only if this is the last cell, and the message is outgoing. Think about this for a second: It doesn't make sense to show the read status of every message. If the receiver has read the last message, you can assume they also read all previous messages. Also, you don't want to show delivery status for their messages, only your own.


You can run the project now, but you won't see much of a difference, even after all this code!


Don't worry! You didn't do anything wrong. You're just not yet listening for changes for the message's delivery status. Let's fix that.

Receiving events

To show if a message was delivered or sent, you first need to get that information from CometChat.


Modifying Chat Service


You'll start by opening ChatService.swift. You'll add a new closure that will be called whenever the delivery status of a message changes. Add a new property to the class, right after the declaration of other closures like onReceivedMessage:


var onMessageStatusChanged: ((String, Message.DeliveryStatus) -> Void)?


You will call this closure whenever a message changes from unknown to delivered or from delivered to read. The closure takes two arguments, a String for the message's ID, and the delivery status of the message.


Next, scroll down to the CometChatMessageDelegate extension and add the following two methods:


func onMessagesDelivered(receipt: MessageReceipt) {

 DispatchQueue.main.async {

   self.onMessageStatusChanged?(receipt.messageId, .delivered)

 }

}

func onMessagesRead(receipt: MessageReceipt) {

 DispatchQueue.main.async {

   self.onMessageStatusChanged?(receipt.messageId, .read)

 }

}


These methods are part of the CometChatMessageDelegate protocol, and CometChat will call them when the status of your message changes: calling onMessagesDelivered or onMessagesRead when their corresponding events happen. In the methods, you'll switch to the main queue and call the closure you just declared.


Now that you're tracking message delivery status in the chat service, you can connect your view controller to listen to those changes. Open ChatViewController.swift, and add this code to the bottom of viewDidLoad:


ChatService.shared.onMessageStatusChanged = { [weak self] messageID, status in

 guard

   let self = self,

   let messageIndex = self.messages.firstIndex(where: { $0.id == messageID })

 else {

   return

 }

 self.messages[messageIndex].deliveryStatus = status

}


Here, you assign the closure that you declared in ChatService. Whenever the status of a message changes, you'll first find that message in your array of messages. Once you find it, you'll change its status. messages has an attached didSet property observer which will reload the table view whenever one of the messages changes.


Run the project on two devices or two simulators. In the first simulator, log in as superhero1 (Iron Man) and start chatting with Captain America. On the other simulator, log in as superhero2 (Captain America) and start chatting with Iron Man.



As you're chatting away, you'll see "Delivered" appear underneath your messages. Congrats, you just added delivery receipts to your iOS chat app!


You'll notice the message never appears as being read, though. CometChat automatically marks messages as delivered when they arrive at the other user's device. However, you control when you want to mark messages as read.

Marking as read

Not all chat apps mark messages as read under the same conditions. For business-oriented chat you need to be absolutely sure a message was read to mark it so – you don't want your users missing important messages! For chatting with friends, though, it's okay to mark a message as read as soon as its loaded.


That's why CometChat gives you control over marking messages as read in a way that works best for your app. Let's start marking messages as read.


Open ChatService.swift and add a new method to the class:


func markAsRead(message: Message) {

 guard let messageID = Int(message.id) else {

   return

 }

 CometChat.markAsRead(messageId: messageID, receiverId: message.user.id, receiverType: .user)

}


This method receives a message and tells CometChat that the currently logged in user read that message.


Now, you can call this message from your UI. Open ChatViewController.swift and, in viewDidLoad, add the line marked below to the assignment of onRecievedMesssage:


ChatService.shared.onReceivedMessage = { [weak self] message in

 guard let self = self else { return }

 let isFromReceiver = message.user == self.receiver

 if !message.isIncoming || isFromReceiver {

   self.messages.append(message)

   self.scrollToLastCell()

   // Add this line:

   ChatService.shared.markAsRead(message: message)

 }

}


Be sure to add the line inside the if block. By adding this line, you make the app marks each incoming message as read as soon as the view controller adds to the messages array.


Next, add the following few lines inside the getMessages callback in viewWillAppear:


ChatService.shared.getMessages(from: receiver) { [weak self] messages in

 self?.messages = messages

 self?.scrollToLastCell()

 // Add this code:

 if let lastIncomingMessage = messages.last(where: { $0.isIncoming }) {

   ChatService.shared.markAsRead(message: lastIncomingMessage)

 }

}


When the view appears, the view controller fetches previous messages between the logged-in user and the receiver. Once the messages are fetched, you'll grab the last incoming message and mark it as read. When you mark a message as read, CometChat automatically marks all previous messages as read, too – so you only need to mark the last one.


Run the app again and do the same dance of chatting between superhero1 and superhero2.



First, you'll see your last message marked as read. As you send more messages, all of them will appear as read, too! You now have functioning read and delivery receipts in your iOS chat app!

Conclusion

You can find the finished project on GitHub.


During this tutorial you've learned how to use CometChat to show read and delivery receipts in your iOS chat app. You added delivery status to your Messagestruct, connected that to your views and then learned how to track and send a message's delivery status to and from CometChat.


By adding read receipts, you give your users a more engaging chat experience, letting them know when to expect an immediate response and instilling a feel of real-time conversation that isn't there without these receipts.


To go even a step further, think about letting your users manually mark messages as read. This can be done by using a long press gesture or a swipe on the contact to reveal a table view edit action. Once the user taps the button to mark a message as read, call ChatService.shared.markAsRead as described above.


You can also add even more UX improvements using CometChat Extensions. We've got a few tutorials to get you started with those:



I hope this tutorial will help you bring a better iOS chat experience to your users trough read and delivery receipts!