Adding a chat message counter to your iOS app using the Chat SDK
With the release of the Chat SDK v2 in April of 2020, many of you may be asking where the floating message counter went. In Chat SDK v1, the counter showed users how many new messages they'd received while not on the Chat screen. This is a pattern that we decided to move away from in Chat SDK v2 for a number of reasons. The primary reason is that providing a floating button directly in apps caused a lot of design issues that quickly became hard for integrators to resolve.
But there's good news. Chat SDK v2 has the ability to retrieve the data that you need to create a message counter button of your own using the following steps for iOS.
The API to implement this feature is currently only available on iOS. The article will be updated with Android code snippets once they are available.
Getting started
This tutorial assumes you already have some knowledge about how the Chat SDK is embedded and implemented. If you haven't embedded the SDK before, see the Getting Started Guide before progressing through this article.
Initial setup
There are a few steps that need to be implemented before diving into the core logic of the feature.
-
You'll need access to the Zendesk Chat dashboard to get the Chat account key. If you don't have access, ask somebody who does to retrieve it for you.
-
Embed the frameworks into your application. This can be done manually or through your preferred dependency manager. See Adding the Chat SDK for more details.
-
Add the initialization code to your AppDelegate.swift file. First, import the API provider layer of the SDK:
import ChatProvidersSDK
Second, initialize the Chat instance with your Chat account key in the didFinishLaunch delegate method:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Chat.initialize(accountKey: "your_account_key")
return true
}
Once you've followed these steps, you're ready to dive into the message counter feature. See the documentation for more details on the previous steps.
Creating a Zendesk wrapper
You'll probably want to customize the look and flow of users' chat experience. This can be done through the Configuration classes and the Chat APIs. As a result, a lot of Zendesk-related code can become spread across your code base. We recommend creating a wrapper that can contain the logic of your Zendesk related code.
import ChatSDK
import ChatProvidersSDK
import CommonUISDK
import MessagingSDK
final class ZendeskChat {
static let instance = ZendeskChat()
private var chatConfiguration: ChatConfiguration {
let chatConfiguration = ChatConfiguration()
chatConfiguration.isOfflineFormEnabled = false
chatConfiguration.isAgentAvailabilityEnabled = true
chatConfiguration.chatMenuActions = [.endChat]
chatConfiguration.preChatFormConfiguration = .init(name: .required,
email: .optional,
phoneNumber: .hidden,
department: .hidden)
return chatConfiguration
}
private var messagingConfiguration: MessagingConfiguration {
let messagingConfiguration = MessagingConfiguration()
// You can change the name of the bot that appears in the chat here.
messagingConfiguration.name = "your_brand_bot_name"
return messagingConfiguration
}
private var chatAPIConfiguration: ChatAPIConfiguration {
let chatAPIConfiguration = ChatAPIConfiguration()
chatAPIConfiguration.department = "Sales"
chatAPIConfiguration.tags = ["iOS", "v2", "messageCounter"]
return chatAPIConfiguration
}
func setupStyle() {
CommonTheme.currentTheme.primaryColor = .black
}
func createChatViewController() throws -> UIViewController {
setupStyle()
let chatEngine = try ChatEngine.engine()
Chat.instance?.configuration = chatAPIConfiguration
let viewController = try Messaging.instance.buildUI(engines: [chatEngine],
configs: [messagingConfiguration,
chatConfiguration])
return viewController
}
}
Exposing Zendesk events to the rest of your app
You'll probably want to leverage some of these Zendesk-related events in parts of your app. To do this in a consolidated and confined approach, we recommend creating a delegate to handle these events appropriately. To decouple the message counter logic from your Zendesk wrapper, you should create a ZendeskChatDelegate delegate.
Add the following delegate to the top of ZendeskChat.swift:
protocol ZendeskChatDelegate: class {
func willShowConversation(for chat: ZendeskChat)
func willCloseConversation(for chat: ZendeskChat)
func unreadMessageCountChanged(numberOfUnreadMessages: Int, in chat: ZendeskChat)
}
Add a delegate property to your wrapper.
weak var delegate: ZendeskChatDelegate?
Listening to user events
The Chat SDK interacts with the Unified SDK through the ChatEngine class that is instantiated and passed into the viewController build method. Among other things, the ChatEngine controls the lifecycle of the chat WebSocket connection. The current implementation of the ChatEngine disconnects the WebSocket connection once the user leaves the chat screen. This is intended behavior.
To implement the unread message counter, you need to reopen this connection and manage the state of the connection when the user leaves the screen or the app. The Unified SDK allows you to listen to user events through the MessagingDelegate. You'll leverage this API to know when a user performs certain events.
Extend your ZendeskChat wrapper to conform to the MessagingDelegate protocol and implement its methods. At the moment, you should only care about the following events: viewWillAppear, viewWillDisappear, and viewControllerDidClose. These can have empty implementations for now.
extension ZendeskChat: MessagingDelegate {
func messaging(_ messaging: Messaging, didPerformEvent event: MessagingUIEvent, context: Any?) {
switch event {
case .viewWillAppear:
delegate?.willShowConversation(for: self)
case .viewWillDisappear:
delegate?.willCloseConversation(for: self)
case .viewControllerDidClose:
break
default:
break
}
}
func messaging(_ messaging: Messaging, shouldOpenURL url: URL) -> Bool {
true // default implementation opens in Safari }
}
You need to inform the Messaging instance that the ZendeskChat wrapper is the class that will handle the user events. Add an initializer to ZendeskChat and assign the instance of the delegate to the Messaging instance.
init() {
Messaging.instance.delegate = self
}
Now the Zendesk wrapper is practically ready to implement the message counter.
Creating the message counter
The Zendesk wrapper will be extended and leveraged to implement the message counter later in this tutorial. First off, you'll need to some UI to showcase the feature unread counter feature. Download MessageOverlay.swift and drag it into your project. This file contains a subclass of UIWindow that you can use to display the new messages and relevant updates.
Once you've added the UI layer, you're ready to start implementing the core functionality of the message counter.
Observing notifications
For this feature to work with unexpected behavior, you need to handle the UIApplication events, disconnecting and connecting appropriately. To do this, we've prepared a helper protocol for you to implement. Download NotificationCenterObserver.swift and drag it into your project.
This protocol has default implementations to help simplify the class that handles the UI events and WebSocket connection. Once you've added it, you'll be ready to create the Message counter class.
Chat connection and state observers
You now have a well established Chat wrapper and a UI layer to implement the feature. But first, you need to piece these two layers together. There are a few steps you need to follow to handle the lifecycle of the chat session and connection. This is due to the aforementioned fact the ChatEngine disconnects from the WebSocket connection when the user leaves the screen.
Create a new file called ZendeskChatMessageCounter.swift. This will handle the core logic of this feature. It has the following responsibilities:
- Store the closure that will be fired every time the message count is fired
- Handle the observation tokens for UIApplication, ChatConnection, and ChatState events.
- Manage the connection life-cycle, through the ConnectionStatus observer, and the ConnectionProvider methods such as connect and disconnect.
- Handle the ChatState updates from the ChatState observer. This is where the unread messages are derived from.
Note: Make sure the class conforms to the NotificationCenterObserver protocol. You don't need to implement the methods. It will use the default implementations for each of them.
import ChatProvidersSDK
import ChatSDK
import MessagingSDK
final class ZendeskChatMessageCounter: NotificationCenterObserver {
// Called every time the unread message count has changed
var onUnreadMessageCountChange: ((Int) -> Void)?
var isActive = false {
didSet {
if isActive == false {
stopMessageCounter()
}
}
}
// MARK: Observations
/// Collection of token objects to group NotificationCentre related observations
var notificationTokens: [NotificationToken] = []
/// Collection of `ObservationToken` objects to group Chat related observations
private var observations: ObserveBehaviours?
// MARK: Chat
private let chat: Chat
private var isChatting: Bool? {
guard connectionStatus == .connected else {
return nil
}
return chatState?.isChatting == true
}
private var chatState: ChatState? {
chat.chatProvider.chatState
}
private var connectionStatus: ConnectionStatus {
chat.connectionProvider.status
}
// MARK: Unread messages
private var lastSeenMessage: ChatLog?
var unreadMessages: [ChatLog]? {
guard
isActive && isChatting == true,
let chatState = chatState,
let lastSeenMessage = lastSeenMessage else {
return nil
}
return chatState.logs
.filter { $0.participant == .agent }
.filter { $0.createdTimestamp > lastSeenMessage.createdTimestamp }
}
private(set) var numberOfUnreadMessages = 0 {
didSet {
if oldValue != numberOfUnreadMessages && isActive {
onUnreadMessageCountChange?(numberOfUnreadMessages)
}
}
}
init(chat: Chat) {
self.chat = chat
}
// To stop observing we have to call unobserve on each observer
private func stopObservingChat() {
observations?.unobserve()
observations = nil
notificationTokens.removeAll()
}
private func observeConnectionStatus() -> ObserveBehaviour {
chat.connectionProvider.observeConnectionStatus { [weak self] (status) in
guard let self = self else { return }
guard status == .connected else { return }
_ = self.observeChatState()
}.asBehaviour()
}
private func observeChatState() -> ObserveBehaviour {
chat.chatProvider.observeChatState { [weak self] (state) in
guard let self = self else { return }
guard self.connectionStatus == .connected else { return }
if state.isChatting == false {
self.stopMessageCounter()
}
if self.isActive {
self.updateUnreadMessageCount()
}
}.asBehaviour()
}
// MARK: Connection life-cycle
private func connect() {
guard connectionStatus != .connected else { return }
chat.connectionProvider.connect()
}
private func disconnect() {
chat.connectionProvider.disconnect()
}
func connectToChat() {
guard isActive else { return }
connect()
startObservingChat()
}
// MARK: Message counter
func startMessageCounterIfNeeded() {
guard isChatting == true && !isActive else { return }
lastSeenMessage = chatState?.logs.last
updateUnreadMessageCount()
}
func stopMessageCounter() {
stopObservingChat()
resetUnreadMessageCount()
disconnect()
}
private func updateUnreadMessageCount() {
numberOfUnreadMessages = unreadMessages?.count ?? 0
}
private func resetUnreadMessageCount() {
numberOfUnreadMessages = 0
}
}
extension ZendeskChatMessageCounter {
// MARK: Observations
//We observe the connection and once it successfully connects we can start observing the state of the chat.
private func startObservingChat() {
observations = ObserveBehaviours(
observeConnectionStatus(),
observeChatState()
)
observeNotification(withName: Chat.NotificationChatEnded) { [weak self] _ in
self?.stopMessageCounter()
}
observeApplicationEvents()
}
private func observeApplicationEvents() {
observeNotification(withName: UIApplication.didEnterBackgroundNotification) { [weak self] _ in
self?.disconnect()
}
observeNotification(withName: UIApplication.willEnterForegroundNotification) { [weak self] _ in
if self?.isActive == true {
self?.connect()
}
}
observeNotification(withName: Chat.NotificationChatEnded) { [weak self] _ in
if self?.isActive == true {
self?.stopMessageCounter()
}
}
}
}
Tying it all together
The heavy lifting is done. Your users will soon be able to receive chat message notifications when they're browsing your app. But there are some small important details that you need to add to your ZendeskChat wrapper before using the feature.
-
Add the following properties to the class:
// MARK: unread message counter
private var messageCounter: ZendeskChatMessageCounter?
var isUnreadMessageCounterActive = false {
didSet {
messageCounter?.isActive = isUnreadMessageCounterActive
}
}
var numberOfUnreadMessages: Int {
messageCounter?.numberOfUnreadMessages ?? 0
}
-
Update the initializer to initialize the messageCounter.
init() {
Messaging.instance.delegate = self
guard let chat = Chat.instance else { return }
messageCounter = ZendeskChatMessageCounter(chat: chat)
messageCounter?.onUnreadMessageCountChange = { [weak self] numberOfUnreadMessages in
guard let self = self else { return }
// Notify delegate
self.delegate?.unreadMessageCountChanged(numberOfUnreadMessages: numberOfUnreadMessages,
in: self)
}
}
-
Update the MessagingDelegate extension to handle the counter appearing and hiding.
func messaging(_ messaging: Messaging, didPerformEvent event: MessagingUIEvent, context: Any?) {
switch event {
case .viewWillAppear:
delegate?.willShowConversation(for: self)
case .viewWillDisappear:
messageCounter?.startMessageCounterIfNeeded()
delegate?.willCloseConversation(for: self)
case .viewControllerDidClose:
messageCounter?.connectToChat()
default:
break
}
}
Once you've done this, your application will successfully manage the lifecycle of the Chat SDK outside of the chat screen. The final step involves conforming to the ZendeskChatDelegate. This should be done in one of your view controllers. This allows you to update the UI accordingly.
-
Add reference to a MessageOverlay object to your ViewController.
private var messageOverlay: MessageCounterOverlay?
-
Update the viewDidLoad to create an instance of the overlay.
override func viewDidLoad() {
super.viewDidLoad()
ZendeskChat.instance.delegate = self
messageOverlay = MessageCounterOverlay()
messageOverlay?.onTap = { [weak self] in
guard let viewController = ZendeskChat.instance.createChatViewController() else { return }
let chatNavigationController = UINavigationController(rootViewController: viewController)
self?.present(chatNavigationController, animated: true, completion: nil)
}
}
-
Implement the delegate methods updating the UI appropriately.
extension ViewController: ZendeskChatDelegate {
func willShowConversation(for chat: ZendeskChat) {
messageOverlay?.hide()
/// Called when conversation will appear
}
func willCloseConversation(for chat: ZendeskChat) {
messageOverlay?.show()
/// Called when conversation will disappear
ZendeskChat.instance.isUnreadMessageCounterActive = true
}
func unreadMessageCountChanged(numberOfUnreadMessages: Int, in chat: ZendeskChat) {
messageOverlay?.updateNewMessageCount(numberOfUnreadMessages)
}
}