We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Add chat functionality to your iOS ticketing app

  • Elena Jovchevska
April 10th, 2019
You will need Xcode 10.1+ and Cocoapods installed on your machine.

Now there is an easy way to integrate chat in your iOS Application. The Chatkit SDK by Pusher is a powerful tool which allows us as developers to easily build multiple chat features and enrich our application. Follow this step by step tutorial for effortless integration and use of the iOS Chatkit SDK.

The iOS application represents a use case in e-commerce, a ticketing system and the integration of a support chat component.

Here is a demo of what we will build:

Prerequisites

For building and running the iOS Application you will need Xcode installed (in this tutorial 10.1 version is used) . You can install it from the link above. If you need to install CocoaPods , run the following command in your console:

    gem install cocoapods

Project setup

After the initial setup is done, the next step is cloning the project from the following link .

In this tutorial we will focus on building out the support chat component. The code for the ticketing component is included in the repository and not covered in this tutorial.

There are two ways to start working with Chatkit and use the SDK in our application, the first one is through CocoaPods, which is a dependency manager for cocoa projects, while the second one is through Carthage, which is decentralized dependency manager. For more details you can checkout the following link. In this project CocoaPods is used. In the project’s folder podfile you will find the current configuration, where chatkit is configured as pod.

The current Podfile, which can be found in the project folder:

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '11.0'
    use_frameworks!

    target 'buytickets' do
      pod 'PusherChatkit'
    end

Whenever you create new iOS project and you were to integrate pods it is necessary to run the following command:

    pod install

This would fetch and install the pods specified in the podfile and create your .xcworkspace which integrates the pods and the main project. Use this for further development. Open the workspace in Xcode.

Your window should look like one above. The project classes are separated in five groups:

  • model
  • view
  • controller
  • resources
  • services

You can take a look in the groups and get to know the code or run the application before continuing with the tutorial. I will go more into details of the support chat component’s implementation.

Chatkit sign up

There is one more step that we did before using the Chatkit SDK in code. And you would need to do if you were to implement Chatkit in your iOS Application. Sign up to the following link , for using the Chatkit services and creating instance which will be later one used in your application.

The first step is to select the option to create instance.

The instance’s name that was created is the same as the name of this sample project, buyTickets.

Now that the instance is created you will get an overview of the instance locator and test token provider, which you can see further in the tutorial that are used in the iOS Application, to use this SDK.

The second step is performed in the Console tab. Here you will be presented with the option to create user.

The first users that were created for the purpose of this tutorial are Agent and Customer. The process of creating the user is very simple.

The console tab now offers more options, such as creating a room for the existing users.

The third step is creating a room for the user you created. The room can be private or public, which is easily managed at this step.

The console again is updated with the rooms overview, such as messages sent in the room, members and options to add new users, members or to delete the room.

To actually create an application flow where we would to have user’s registration, we would need to have a backend. This means we would have a database where we would save the new users, and also the option to create them through API. For the purpose of this tutorial I am using only the Chatkit iOS SDK, so the workaround for mocking a login interface, was to actually save the created user’s ID in NSUserDefaults. This way we would be able to mock the login process and validate whether an existing user has tried to logged in. We will go more into details in the code below.

Chatkit use in our iOS application

Now let’s dive into the code. First open buytickets.xcworkspace and navigate to TicketDetailsViewController.swift

This application has only two use cases. The first one is the option to look at the ticket details as presented in the image below. While the second one is the option to contact support. Which is an interactive one.

Because of having two very different use cases that are navigated with the segmented control, it was decided to use two different tableviews. The only reload of these two different tableviews will happen during update of data.

Each tableview has dedicated delegate classes, implementing the UITableViewDelegate and UITableViewDatasource methods, correspondingly SupportChatTableViewDelegate.swift and TicketDetailsTableViewDelegate.swift. For building the first mock screen there is model named TicketDetailsModel.swift, which can be easily expanded if of need for similar application use case.

Now if we were to take a look at the interface builder we could see that the cells for the tableview are defined as prototype cells. The logic for hiding and showing the dedicated tableview and animating them is handled in TicketDetailsViewController.swift.

After getting basic overview of the UI setup, let us dive more into the code. The first screen the user will see is the Login screen.

The logic behind this one is fairly simple. It was mentioned above that we will save the user credentials in NSUserDefaults, and this is done on func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { method, found in AppDelegate.swift.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            StorageAccess().storeCredentialsInPreferences()
            return true
        }

In StorageAccess.swift for the purpose of this demo we manually save the agent and customer users ID, if not already saved. This way when the user tries to logs in, we can check if the entered username is presented in NSUserDefaults, if it is we log in the user, otherwise we show error alert.

    class StorageAccess {
        private let credentailsKey = "credentials"
        private let defaults = UserDefaults.standard

        func storeCredentialsInPreferences() {
            if defaults.object(forKey: credentailsKey) == nil {
                defaults.set(["Agent1","Customer1"], forKey: credentailsKey)
            }
        }

        func getCredentials() -> [String]? {
            return defaults.object(forKey: credentailsKey) as? [String]
        }        
    }

In the main view controller, TicketDetailsViewController.swift, we define the constants and variables we will need for creating the instance of the ChatManager.

        // Chat vars
        var chatManager: ChatManager!
        var currentUser: PCCurrentUser?
        var chatManagerDelegate: PCChatManagerDelegate?
        var messages: [PCMessageInfo] = []
        var defaultFrame: CGRect!
        var usernameCredential = ""  

        // Constants
        let chatInstanceLocator = "INSTANCE_LOCATOR_KEY"
        let chatTokenProvider = "https://us1.pusherplatform.io/services/chatkit_token_provider/v1/TOKEN_PROVIDER_KEY/token"

You can notice that the constants defined above are the ones that can be found in the Chatkit SDK console of the created instance. For the purpose of this tutorial test token provider was used. Otherwise for production environment, in order to generate your token you should use the secret key listed under CREDENTIALS.

The first step would be to load the initial data, setup the tableview’s delegates, initialize the UI elements, register for notifications and setup the chat manager instance. This is done in the viewDidLoad() method.

    override func viewDidLoad() {
            super.viewDidLoad()
            // Load the selected ticket details
            loadTicketDetails()

            // Initialize the tableViewDelegates
            setupTableViewDelegateForTicketDetails()
            setupTableViewDelegateForSupportChat()

            // Setup ticketDetails header view
            setupTicketDetailsTopView()

            //Setup chat manager for the support chat
            setupChatManager()

            // Initialize the default frame
            defaultFrame = self.view.frame

            //Register for notifications
            NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
        }

The reason we register for keyboard notifications and save the default frame is for performing the expand and compress animation on the support chat screen. Can be observed in the demo above.

When setting up the chat manager we would use the constants defined above. Each chat manager instance is created with one user ID. So for example we could run one instance of the chat manager with Agent1 ID on simulator iPhone XR and other one with Customer1 ID on simulator iPhone 7, and simulate a conversation since the connection is open to their shared room. Presented in a demo below.

This is the complete code section for setup of chat manager, but below we will go step by step into the code.

        func setupChatManager() {
            chatManager = ChatManager(
                instanceLocator: chatInstanceLocator,
                tokenProvider: PCTokenProvider(url: chatTokenProvider),
                userID: usernameCredential
            )

            self.chatManagerDelegate = SupportChatManagerDelegate(self, 
                                                                  usernameCredential)
            guard let chatManagerDelegate = self.chatManagerDelegate else { return }
            chatManager.connect(delegate: chatManagerDelegate) { [unowned self] 
                                                                 currentUser, error in
                guard error == nil else {
                    self.handleTechnicalError("Error connecting")
                    return
                }

                guard let currentUser = currentUser else { return }
                self.currentUser = currentUser
                guard let rooms = self.currentUser?.rooms else { return }
                if rooms.count > 0 {
                    self.subscribeUser(to: rooms[0])
                } else {
                    currentUser.getJoinableRooms(completionHandler: { (rooms, error) in
                        guard let rooms = rooms else { return }
                        self.joinUser(to: rooms[0])
                    })
                }
            }
        }

        private func subscribeUser(to room: PCRoom) {
            currentUser?.subscribeToRoom(
                room: room,
                roomDelegate: self,
                messageLimit: 0
            ) { error in
                guard error == nil else {
                    self.handleTechnicalError("Error subscribing to room")
                    return
                }
            }
        }

        private func joinUser(to room: PCRoom) {
            currentUser?.joinRoom(room,
                                 completionHandler: { (room, error) in
                                    guard error == nil else {
                                        self.handleTechnicalError("Error subscribing to room")
                                        return
                                    }
            })
        }

We will go step by step from the above code section. First we initialize chatManager instance, using the constants defined above and the usernameCredential fetched at login.

    chatManager = ChatManager(
                instanceLocator: chatInstanceLocator,
                tokenProvider: PCTokenProvider(url: chatTokenProvider),
                userID: usernameCredential
            )

Next we would need chatManagerDelegate class, one that will implement PCChatManagerDelegate.

    self.chatManagerDelegate = SupportChatManagerDelegate(self, usernameCredential)
    guard let chatManagerDelegate = self.chatManagerDelegate else { return }

From PCChatManagerDelegate we implement onUserStartedTyping and onUserStoppedTyping, so we can show the typing indicator on the support chat. For this purpose we implement the protocol ChatManagerTypingDelegate in TicketDetailsViewController.swift.

    // Protocol updating TicketDetailsViewController for PChatManagerDelegate typing method's updates
    protocol ChatManagerTypingDelegate {
        func didChangeTypingState(for content: String)
    }

    // Custom class implementing the PChatManagerDelegate for the support chat
    class SupportChatManagerDelegate: PCChatManagerDelegate {
        private let delegate: ChatManagerTypingDelegate
        private let currentUsername: String

        init(_ delegate: ChatManagerTypingDelegate,
             _ currentUsername: String) {
            self.delegate = delegate
            self.currentUsername = currentUsername
        }

        func onUserStartedTyping(inRoom room: PCRoom,
                                 user: PCUser) {
            if currentUsername != user.id {
                let message = String.init(format:"%@ started typing...", user.name ?? "")
                delegate.didChangeTypingState(for: message)
            }
        }

        func onUserStoppedTyping(inRoom room: PCRoom,
                                 user: PCUser) {
            if currentUsername != user.id {
                delegate.didChangeTypingState(for: "")
            }
        }
    }

You will notice that the support chat has a textField and a send button. Whenever we start typing in the textField an IBAction is fired, that can be found in TicketDetailsViewController.swift

    @IBAction func textFieldDidChange(_ sender: Any) {
            guard let currentUser = self.currentUser else {return}
            currentUser.typing(in: currentUser.rooms[0].id,
                               completionHandler: { error in
                guard error == nil else {
                    self.handleTechnicalError("Error occured")
                    return
                }
            })
        }

This provokes the userStartedTyping method from PCChatManagerDelegate

    func onUserStartedTyping(inRoom room: PCRoom,
                                 user: PCUser) {
            if currentUsername != user.id {
                let message = String.init(format:"%@ started typing...", user.name ?? "")
                delegate.didChangeTypingState(for: message)
            }
        }

And that is when the typing indicator is animated on the screen.

    //MARK: ChatManagerTypingDelegate
        func didChangeTypingState(for content: String) {
            DispatchQueue.main.async {
                // UILabel must be updated from main thread
                self.typingIndicatorLabel.text = content
            }
        }

The following step after setting up of PCChatManagerDelegate is connecting the chatManager instance, by sending the delegate as argument.

    chatManager.connect(delegate: chatManagerDelegate) { [unowned self] 
                                                                 currentUser, error in
                guard error == nil else {
                    self.handleTechnicalError("Error connecting")
                    return
                }

                guard let currentUser = currentUser else { return }
                self.currentUser = currentUser
                guard let rooms = self.currentUser?.rooms else { return }
                if rooms.count > 0 {
                    self.subscribeUser(to: rooms[0])
                } else {
                    currentUser.getJoinableRooms(completionHandler: { (rooms, error) in
                        guard let rooms = rooms else { return }
                        self.joinUser(to: rooms[0])
                    })
                }
            }

In the response of the connect(delegate:) method we receive the currentUser for the specified usernameCredential. The next step is to check the currentUser rooms. There are two options for the user’s rooms. If we check for Agent1, user which is already subscribed to Support room, the program will return that the user has one room and we call the method subscribeUser.

    private func subscribeUser(to room: PCRoom) {
            currentUser?.subscribeToRoom(
                room: room,
                roomDelegate: self,
                messageLimit: 0
            ) { error in
                guard error == nil else {
                    self.handleTechnicalError("Error subscribing to room")
                    return
                }
            }
        }

In this method the currentUser is subscribed to the room, sending as arguments the room it wants to subscribe, the class that implements the PCRoomDelegate, which in this tutorial is TicketDetailsViewController.swift and the messageLimit, which indicates whether we will show the old messages of the room, when a new connection happens. In this case messageLimit: 0 indicates that previous messages will not be shown.

But if we were to check for Customer1, which is not subscribed to any rooms, the program would return that the number of rooms for the currentUser is zero, therefore we call the method getJoinableRooms. This will return all joinable rooms, but since in our console we created only one room, the Support room, the rooms object contains only that room. The next step is for Customer1 user to join that room, with the method call self.joinUser(to: rooms[0])

    private func joinUser(to room: PCRoom) {
            currentUser?.joinRoom(room,
                                 completionHandler: { (room, error) in
                                    guard error == nil else {
                                        self.handleTechnicalError("Error subscribing to room")
                                        return
                                    }
            })
        }

After this method finishes successfully and if we were to open the online console for the Support room, we would see that now there are two users subscribed for the room. Agent1 and Customer1.

After we connected the manager, fetched the user and subscribed to the room, let us see the process of sending and handling a message. The next IBAction is the one that the send button triggers.

    @IBAction func sendMessageButton(_ sender: Any) {
            guard let currentUser = self.currentUser else {return}
            currentUser.sendMessage(
                roomID: currentUser.rooms.first?.id ?? "",
                text: self.supportChatInput.text ?? ""
            ) { messageId, error in
                guard error == nil else {
                    self.handleTechnicalError("Error sending message")
                    return
                }
            }
            self.supportChatInput.text?.removeAll()
        }

This method initiate sendMessage for the currentUser, sending the user’s room and text message as an arguments. Finally we reset the textField’s text.

Calling sendMessage method will provoke the onMessage(_ message: PCMessage) method from PCRoomDelegate, implemented in TicketDetailsViewController.swift.

    func onMessage(_ message: PCMessage) {
            let isReceivedMessage = !(message.sender.id == chatUserID)
            let messageInfo = PCMessageInfo.init(message: message,
                                                 received: isReceivedMessage)
            messages.insert(messageInfo, at: 0)
            DispatchQueue.main.async {
                self.supportChatTableViewDelegate?.update(self.messages)
                self.supportChatTableView.reloadData()
            }
        }

Here we mark whether the messages’s sender ID is the same as the chatUserID, therefore marking whether the current message was sent or received. Then we will create the struct PCMessageInfo constructed of the message string and the information whether the message is received or sent.

    // Struct defining whether the current message is received or the send one
    struct PCMessageInfo {
        var message: PCMessage
        var received: Bool
    }

Next we will update the messages list and update the support chat tableview content.

    DispatchQueue.main.async {
                self.supportChatTableViewDelegate?.update(self.messages)
                self.supportChatTableView.reloadData()
            }

It is really important that we would do this on main queue, since we are going to update the UI.

The place were the UI of the support chat is being set up and updated is the SupportChatTableViewDelegate.swift , so let’s take a look at it.

    class SupportChatTableViewDelegate: NSObject, UITableViewDelegate, UITableViewDataSource {
        private weak var tableView: UITableView?
        private var messages: [PCMessageInfo] = []

        init(_ tableView: UITableView,
             _ messages: [PCMessageInfo]) {
            self.tableView = tableView
            self.messages = messages
            super.init()
            self.tableView?.delegate = self
            self.tableView?.dataSource = self
            self.tableView?.transform = CGAffineTransform(scaleX: 1, y: -1)
            self.tableView?.register(UITableViewCell.self,
                                   forCellReuseIdentifier: "Cell")
            self.tableView?.rowHeight = UITableView.automaticDimension
            self.tableView?.estimatedRowHeight = 50
        }

        func update(_ messages: [PCMessageInfo]) {
            self.messages = messages
        }

This class if implementing tableview methods and defining its UI. It is a simple implementation, drawing tableView with number of rows corresponding the messages sent. Then using UITableViewCell with only one label, so to present the sent message.

    //MARK: TableViewDelegate
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }

        func tableView(_ tableView: UITableView,
                       numberOfRowsInSection section: Int) -> Int {
            return messages.count
        }

        func tableView(_ tableView: UITableView,
                       cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell",
                                                     for: indexPath)
            cell.backgroundColor = .clear
            cell.textLabel?.textColor = .white
            let messageInfo = self.messages[indexPath.row]
            cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
            if messageInfo.received {
                cell.textLabel?.textAlignment = .left
                cell.textLabel?.text = "\(messageInfo.message.sender.displayName): \(messageInfo.message.text)"
            } else {
                cell.textLabel?.textAlignment = .right
                cell.textLabel?.text = "\("You:") \(messageInfo.message.text)"
            }
            cell.selectionStyle = .none
            return cell
        }

        func tableView(_ tableView: UITableView,
                       heightForRowAt indexPath: IndexPath) -> CGFloat {
            return UITableView.automaticDimension
        }

        func tableView(_ tableView: UITableView,
                       heightForFooterInSection section: Int) -> CGFloat {
            return 0.1
        }

        func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
            return 0.1
        }

In the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) method, you can notice that the flag received is used for the content alignment of the message info, so we can achieve the visual clarity in the support chat. Another thing is using UITableView.automaticDimension, so we wouldn’t need to limit the message size.

Chatkit realtime use case

The final step is seeing how all of this would look like when we create two instances of users and Chatkit. Let’s observe the outcome of this implementation.

Conclusion

Hope you enjoyed my tutorial. The whole code can be downloaded on the following GitHub. You should have a better overview of using the Chatkit iOS SDK and understanding one of its many use cases. The above application code can be easily extended for your e - commerce application. Also you can read the rest of the Chatkit documentation for the iOS SDK and implement more of the functionalities it offers, besides the above presented ones.

Clone the project repository
  • Chat
  • iOS
  • Swift
  • Chatkit

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.