In an article published earlier, we explored how to use SlackTextViewController and Chatkit to create a messaging application. This application allows us to share messages with others in a chat room. In this article, however, we will explore how you can use Chatkit with UITableViewController without the need for any third party library.

Here’s a screen recording of what we will be creating:

Let’s get started.

Requirements

Before we begin with the article, you should have the following prerequisites:

  • Xcode 7 or higher.
  • Knowledge of Swift and Xcode interface builder.
  • Cocoapods installed on your machine.
  • Node.js and NPM installed on your machine.
  • Basic knowledge of JavaScript (Node.js and Express).
  • Basic knowledge on Chatkit. Check out the docs here.

If you have all the requirements, then let us start.

Setting Up Chatkit

Go to the Chatkit page. Create an account and create a Chatkit application from the dashboard.

Follow the “Getting Started” wizard until the end so that it helps you create a new user account and a new chat room.

On that same screen, after you have completed the “Getting Started” wizard, click on “Keys” to get your application’s ‘Instance Locator’ and ‘Key’. You will need these values to make requests to the Chatkit API.

That’s all that is required to set up Chatkit. Now let’s create our application.

Creating our Chatkit Application in Xcode

Launch Xcode and create a new “Single Page” project. Follow the wizard until you see the Xcode workspace. Now, close Xcode and launch your Terminal application.

Installing Packages Using Cocoapods
In your Terminal application, cd to the root of your application’s directory and run the command below to initialise Cocoapods:

$ pod init

This will create a Podfile in the root of your application’s directory. The Podfile is where we will define dependencies we need Cocoapods to install and make available to our iOS application.

Open the Podfile and replace the contents of the file with the code below:

platform :ios, '10.0'

target 'chatroom' do
  use_frameworks!
  pod 'PusherChatkit'
  pod 'Alamofire'
end

Save the file and go back to your Terminal application. From the root directory of your application, run the command below:

 $ pod install

This will add the PusherChatkit swift SDK to your application’s workspace. This will also add Alamofire which we will use to make requests to our Node.js backend.

Once the installation is complete, open the *.xcworkspace file that was generated by Cocoapods. This will automatically launch Xcode.

Creating Your Application’s Views and Controllers
The next thing we want to do is create the views for our application. Open the Main.storyboard file. In the initial View Controller in the scene, add a button and a text field.

Here is what we have created in the first View Controller scene:

For now, the button and the text view do not do anything. Create a new view controller file in Xcode called AuthViewController. This view controller is where we will do all the necessary authentication and provision our user with a token to make calls to the Chatkit API.

After creating the view controller, open your Main.storyboard and make the controller the custom class for the view.

Open the “Show Assistant” menu so that the Main.storyboard and AuthViewController windows line up side by side. Now create an @IBOutlet for both the text field and the button.

Now, in the controller, add the block below after the viewDidLoad method:

var username: String!

@objc func textDidChange(_ sender: Any?) -> Void {
  loginButton.isEnabled = textField.text!.characters.count >= 3     
}

@objc func loginButtonWasPressed(_ sender: Any?) -> Void {
    textField.isEnabled = false
    loginButton.isEnabled = false

    login(username: textField.text!) { (response) in
        self.username = self.textField.text!
        self.performSegue(withIdentifier: "loadRoomsTableViewController", sender: self)
    }
}

private func login(username: String, completion: @escaping (_ response: Any?) -> Void) {
    // code
}

These methods will serve as callbacks for the events fired when the text is edited and the button is pressed.

To register the callbacks to the events, paste the code below at bottom of the viewDidLoad method:

// The button is disabled by default
loginButton.isEnabled = false

// Register callbacks for certain eventsx
textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
loginButton.addTarget(self, action: #selector(loginButtonWasPressed), for: .touchUpInside) 

Now any time the button is pressed the loginButtonWasPressed method will be called. Also, when the username field is altered, the textDidChange method will be called.

Let us add some flesh to the login method so it makes a request to the Node.js endpoint. Replace the login method with the following:

private func login(username: String, completion: @escaping (_ response: Any?) -> Void) {
    let payload: Parameters = ["username": username]

    let request = Alamofire.request(AppConstants.ENDPOINT + "/login", method: .post, parameters: payload)

    request.validate().responseJSON { (response) in
        switch response.result {
        case .success(_):
            completion(response.data!)
        case .failure(let error):
            print(error)
        }
    }
}

In the above, we use the Alamofire library to send a POST request to AppConstants.ENDPOINT + "``/login``" (we will define it soon). When we get the response.data, we then pass it to the completion callback which will then be handled in the loginButtonWasPressed method.

At the top of the file add import '``Alamofire``' so the controller will work:

import 'Alamofire'

Now open the AppDelegate.swift class and add this after the import statement:

struct AppConstants {
    static let ENDPOINT    = "http://localhost:4000";
    static let INSTANCE_LOCATOR = "PUSHER_CHATKIT_INSTANCE_LOCATOR";
}

The ENDPOINT in this struct will be the remote backend web server. We will create this backend later using Node.js. The *PUSHER_CHATKIT_INSTANCE_LOCATOR* should be replaced with the instance locator provided for your Chatkit application in your Pusher Chatkit dashboard.

Let’s add one final method to the AuthViewController:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "loadRoomsTableViewController" {
        let ctrl = (segue.destination as! UINavigationController).viewControllers.first as! RoomsTableViewController
        ctrl.username = username
    }
}

In this method, we set the username property to the controller we will be navigating to before we get there.

Create a new RoomTableViewController and replace the contents with the following:

import UIKit
import PusherChatkit

class RoomsTableViewController: UITableViewController, PCChatManagerDelegate {

    var username: String!

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Select a Room"
    }
}

Open the Main.storyboard and add a new navigation controller to the board. The next thing you need to do is create a manual segue from the *RoomsTableViewController* controller to the navigation controller. Call this segue *loadRoomsTableViewController*.

Now, on the “Root View Controller” of the navigation controller, set the custom class to RoomsViewController. You should have something like this now:

Next, give the table cells a reuse identifier of RoomCell. Note that I have set my “Table View Cell” type to subtitle.

Connecting our Application to Chatkit
Open your RoomsViewController. Let’s make our application connect to Chatkit.

First, declare some properties we will use in the controller class. After the username property, add the following properties:

var rooms = [PCRoom]()
var selectedRoom: PCRoom!
var currentUser: PCCurrentUser!

The rooms will hold multiple PCRoom objects, the selectedRoom will hold a PCRoom object for the selected room, and the currentUser will be a PCCurrentUser object.

Now, add the method below to the controller:

private func initPusherChatkitManager(completion: @escaping (_ success: PCCurrentUser) -> Void) {
    let chatkit = ChatManager(
        instanceId: AppConstants.INSTANCE_LOCATOR,
        tokenProvider: PCTokenProvider(url: AppConstants.ENDPOINT+"/authenticate", userId: username)
    )

    chatkit.connect(delegate: self) { (user, error) in
        guard error == nil else { return }
        guard let user = user else { return }

        DispatchQueue.main.async {
            completion(user)
        }
    }
}

This method will instantiate Chatkit and connect to the service. We use a token that will be returned from our remote endpoint to make requests. When a successful connection has been established, a completion handler is called. At this point, we can make authenticated calls to Chatkit.

Let us actually use this method we just created. In your viewDidLoad method, add this to the bottom:

initPusherChatkitManager { user in
    self.currentUser = user

    user.getAllRooms() { (rooms, error) in
        guard error == nil else { return }
        self.rooms = rooms!

        rooms?.forEach { room in
            user.joinRoom(room) { room, error in
                guard error == nil else { return }
            }
        }

        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }

    self.tableView.reloadData()
}

Now let us display the actual data in our UITableViewController. Add the following methods below to make our response the data source for the table view controller.

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "RoomCell", for: indexPath)

    cell.textLabel?.text = rooms[indexPath.row].name
    cell.detailTextLabel?.text = rooms[indexPath.row].isPrivate ? "Private" : "Public"

    return cell
}

In the first method, we return the count of rooms available to be joined. In the second method, we make sure each cell is configured to show the name and visibility of the room.

Now, we want it so that when a cell is clicked it should load a room (a new view controller) with all the messages that have been posted to that room. Add the methods below to the controller:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    selectedRoom = rooms[indexPath.row]
    performSegue(withIdentifier: "loadRoomTableViewController", sender: self)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "loadRoomTableViewController" {
        let ctrl = segue.destination as! RoomTableViewController
        ctrl.room = selectedRoom
        ctrl.currentUser = currentUser
    }
}

The first method sets the selectedRoom object and then calls the performSegue method. The second method prepares our destination controller before we get there. Our preparation includes setting the selectedRoom object on the controller and setting the currentUser.

Our controller should look like this now:

import UIKit
import PusherChatkit

class RoomsTableViewController: UITableViewController, PCChatManagerDelegate {

    var username: String!
    var rooms = [PCRoom]()
    var selectedRoom: PCRoom!
    var currentUser: PCCurrentUser!

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Rooms"

        initPusherChatkitManager { user in
            self.currentUser = user

            user.getAllRooms() { (rooms, error) in
                guard error == nil else { return }
                self.rooms = rooms!

                rooms?.forEach { room in
                    user.joinRoom(room) { room, error in
                        guard error == nil else { return }
                    }
                }

                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }

            self.tableView.reloadData()
        }
    }

    private func initPusherChatkitManager(completion: @escaping (_ success: PCCurrentUser) -> Void) {
        let chatkit = ChatManager(
            instanceId: AppConstants.INSTANCE_LOCATOR,
            tokenProvider: PCTokenProvider(url: AppConstants.ENDPOINT+"/authenticate", userId: username)
        )

        chatkit.connect(delegate: self) { (user, error) in
            guard error == nil else { return }
            guard let user = user else { return }

            DispatchQueue.main.async {
                completion(user)
            }
        }
    }


    // MARK: - Table view data source

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "RoomCell", for: indexPath)

        cell.textLabel?.text = rooms[indexPath.row].name
        cell.detailTextLabel?.text = rooms[indexPath.row].isPrivate ? "Private" : "Public"

        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        selectedRoom = rooms[indexPath.row]
        performSegue(withIdentifier: "loadRoomTableViewController", sender: self)
    }

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "loadRoomTableViewController" {
            let ctrl = segue.destination as! RoomTableViewController
            ctrl.room = selectedRoom
            ctrl.currentUser = currentUser
        }
    }
}

Now let us create the room view. This will be where we will see all the messages that are specific to a room.

Create a new view controller class called RoomTableViewController. Replace the contents of the file with this:

import UIKit
import PusherChatkit

class RoomTableViewController: UITableViewController, PCRoomDelegate {

    var room: PCRoom!
    var messages = [PCMessage]()
    var currentUser: PCCurrentUser!

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = room.name

        // Subscribe to room
        currentUser.subscribeToRoom(room: room, roomDelegate: self)
    }
}

Go to your Main.storyboard and add a new UITableViewController scene. Make the new class you just created the custom class for the scene.

Click on the new view controller on the storyboard and go to the “Connections Inspector” in the right sidebar. Drag from the “show” panel to the RoomsTableViewController to create a manual segue between them.

Set the identifier of the segue to loadRoomTableViewController. The next thing we want to do is create a custom table view cell class. This will be where we will customise the look of our table view cells for messages.

Create a new class called MessageTableViewCell and extend the UITableViewCell class. In that class paste the contents below:

import UIKit

class MessageTableViewCell: UITableViewCell {

    static let kMessageTableViewCellMinimumHeight: CGFloat = 50.0;
    static let kMessageTableViewCellAvatarHeight: CGFloat = 30.0;

    static let CellIdentifier: String = "MessageCell";

    var _bodyLabel: UILabel?
    var _titleLabel: UILabel?
    var _thumbnailView: UIImageView?

    var indexPath: IndexPath?
    var usedForMessage: Bool?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setup()
    }

    func setup() {
        self.selectionStyle = .none
        self.backgroundColor = UIColor.white

        configureSubviews()
    }

    func configureSubviews() {
        contentView.addSubview(thumbnailView())
        contentView.addSubview(titleLabel())
        contentView.addSubview(bodyLabel())

        let views: [String:Any] = [
            "thumbnailView": thumbnailView(),
            "titleLabel": titleLabel(),
            "bodyLabel": bodyLabel()
        ]

        let metrics = [
            "tumbSize": MessageTableViewCell.kMessageTableViewCellAvatarHeight,
            "padding": 15,
            "right": 10,
            "left": 5
        ]

        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "H:|-left-[thumbnailView(tumbSize)]-right-[titleLabel(>=0)]-right-|",
            options: [],
            metrics: metrics,
            views: views
        ))

        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "H:|-left-[thumbnailView(tumbSize)]-right-[bodyLabel(>=0)]-right-|",
            options: [],
            metrics: metrics,
            views: views
        ))

        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "V:|-right-[thumbnailView(tumbSize)]-(>=0)-|",
            options: [],
            metrics: metrics,
            views: views
        ))

        if (reuseIdentifier == MessageTableViewCell.CellIdentifier) {
            contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|-right-[titleLabel(20)]-left-[bodyLabel(>=0@999)]-left-|",
                options: [],
                metrics: metrics,
                views: views
            ))
        }
        else {
            contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|[titleLabel]|",
                options: [],
                metrics: metrics,
                views: views
            ))
        }
    }

    // MARK: Getters

    override func prepareForReuse() {
        super.prepareForReuse()

        selectionStyle = .none

        let pointSize: CGFloat = MessageTableViewCell.defaultFontSize()

        titleLabel().font = UIFont.boldSystemFont(ofSize: pointSize)
        bodyLabel().font = UIFont.systemFont(ofSize: pointSize)
        titleLabel().text = ""
        bodyLabel().text = ""
    }

    func titleLabel() -> UILabel {
        if _titleLabel == nil {
            _titleLabel = UILabel()
            _titleLabel?.translatesAutoresizingMaskIntoConstraints = false
            _titleLabel?.backgroundColor = UIColor.clear
            _titleLabel?.isUserInteractionEnabled = false
            _titleLabel?.numberOfLines = 0
            _titleLabel?.textColor = UIColor.gray
            _titleLabel?.font = UIFont.boldSystemFont(ofSize: MessageTableViewCell.defaultFontSize())
        }

        return _titleLabel!
    }

    func bodyLabel() -> UILabel {
        if _bodyLabel == nil {
            _bodyLabel = UILabel()
            _bodyLabel?.translatesAutoresizingMaskIntoConstraints = false
            _bodyLabel?.backgroundColor = UIColor.clear
            _bodyLabel?.isUserInteractionEnabled = false
            _bodyLabel?.numberOfLines = 0
            _bodyLabel?.textColor = UIColor.darkGray
            _bodyLabel?.font = UIFont.systemFont(ofSize: MessageTableViewCell.defaultFontSize())
        }

        return _bodyLabel!
    }

    func thumbnailView() -> UIImageView {
        if _thumbnailView == nil {
            _thumbnailView = UIImageView()
            _thumbnailView?.translatesAutoresizingMaskIntoConstraints = false
            _thumbnailView?.isUserInteractionEnabled = false
            _thumbnailView?.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            _thumbnailView?.layer.cornerRadius = MessageTableViewCell.kMessageTableViewCellAvatarHeight / 2.0
            _thumbnailView?.layer.masksToBounds = true
        }

        return _thumbnailView!
    }

    class func defaultFontSize() -> CGFloat {
        return 16.0
    }
}

This class basically sets the design and constraints for the table view cell. If you look into the methods, it just sets height, width and so on. Nothing too special.

Now we need to set this class as the custom class for our table view cell in the RoomTableViewController. Open the storyboard and click on the prototype cell on the latest table view controller. Set the MessageTableViewCell as the custom class for the prototype cell. Next, set the reuse identifier for the cell to MessageCell.

Adding Chatkit Support to the RoomTableViewController
Open the RoomTableViewController. In there we want to do a few things:

  • Display messages available in the channel.
  • Add ability for a user to add a new message.

So let us display the messages first. Add the following method to the controller:

public func newMessage(message: PCMessage) {
    let count = self.messages.count
    let indexPath = IndexPath(row: count, section: 0)
    let rowAnimation: UITableViewRowAnimation = .top
    let scrollPosition: UITableViewScrollPosition = .top

    DispatchQueue.main.async {
        self.tableView.beginUpdates()
        self.messages.insert(message, at: self.messages.count)
        self.tableView.insertRows(at: [indexPath], with: rowAnimation)
        self.tableView.endUpdates()

        self.tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: true)
        self.tableView.reloadRows(at: [indexPath], with: .automatic)
        self.tableView.reloadData()
    }
}

This method is a product of the PCRoomDelegate. It is automatically called every time a message is added to the current room. In the method, we add every message, as it comes in, to the self.messages array. We then reload the table data to see the changes.

⚠️ As the docs say, when making UI updates you need to do it in the main thread. That is why we are dispatching it to the main thread.

To actually display the messages in the table, add the following methods to the controller:

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

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if tableView == self.tableView {
        let message = messages[(indexPath as NSIndexPath).row]

        if message.text.characters.count == 0 {
            return 0
        }

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineBreakMode = .byWordWrapping
        paragraphStyle.alignment = .left

        let pointSize = MessageTableViewCell.defaultFontSize()

        let attributes = [
            NSAttributedStringKey.font: UIFont.systemFont(ofSize: pointSize),
            NSAttributedStringKey.paragraphStyle: paragraphStyle
        ]

        var width = tableView.frame.width - MessageTableViewCell.kMessageTableViewCellAvatarHeight
        width -= 25.0

        let titleBounds = (message.sender.displayName as NSString!).boundingRect(
            with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: attributes,
            context: nil
        )

        let bodyBounds = (message.text as NSString!).boundingRect(
            with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: attributes,
            context: nil
        )

        var height = titleBounds.height
        height += bodyBounds.height
        height += 40

        if height < MessageTableViewCell.kMessageTableViewCellMinimumHeight {
            height = MessageTableViewCell.kMessageTableViewCellMinimumHeight
        }

        return height
    }

    return MessageTableViewCell.kMessageTableViewCellMinimumHeight
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let message = messages[(indexPath as NSIndexPath).row]

    let cell = tableView.dequeueReusableCell(withIdentifier: MessageTableViewCell.CellIdentifier) as! MessageTableViewCell

    cell.bodyLabel().text = message.text
    cell.titleLabel().text = message.sender.displayName

    cell.usedForMessage = true
    cell.indexPath = indexPath
    cell.transform = self.tableView.transform

    return cell
}

In the methods above, we are making sure the table cell picks up the correct data from the self.messages property.

In the *tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath)* method, we calculate the height of the table cell so that long text is wrapped and handled properly.

Now we need to allow people add messages to the chat window. Add the following to the bottom of your viewDidLoad method:

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addMessage))

This will add an “Add” button to the top right of the view. Clicking it will fire the addMessage method. Let’s create that method:

@objc func addMessage() -> Void {
    let alertCtrl = UIAlertController(title: "Add Message", message: "What would you like to say?", preferredStyle: .alert)

    alertCtrl.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

    alertCtrl.addAction(UIAlertAction(title: "Add", style: .default, handler: { action in
        guard let room = self.room else { return }
        guard let message = self.messageField.text else { return }
        guard message.characters.count > 0 else { return }

        self.currentUser?.addMessage(text: message, to: room, completionHandler: { (messsage, error) in
            guard error == nil else { return }
            print("Message added successfully!")
        })
    }))

    alertCtrl.addTextField { text in
        text.placeholder = "Enter message..."
        self.messageField = text
    }

    present(alertCtrl, animated: true)
}

In the method above, we create an alert controller that displays a text field. You can use this form to send your messages and it will be added as seen in the “Add” UIAlertAction‘s callback handler. With that, whenever you add new messages, they will be sent to Chatkit.

That’s all for the iOS application. We will, however, create a demo Node.js application. We will need this to test the app. Let’s do that.

Creating a Node.js Backend for Our Chatkit Application

Create a new directory for our web application. In that directory, create a package.json file with the contents below:

{
  "main": "index.js",
  "dependencies": {
    "body-parser": "^1.17.2",
    "express": "^4.15.3",
    "pusher-chatkit-server": "^0.5.0"
  }
}

Now open terminal and run the command below to start installing the dependencies:

$ npm install

When the installation is complete, create a new index.js file and paste the content below:

// Pull in the libraries
const express    = require('express');
const bodyParser = require('body-parser');
const Chatkit    = require('pusher-chatkit-server');

// Instantiate
const app        = express();
const chatkit    = new Chatkit.default(require('./config.js'))

// Middleware!
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Creates a new user
app.post('/login', (req, res) => {
    let username = req.body.username;
    chatkit.createUser(username, username).then(r => res.json({username})).catch(e => res.json(e))
})

// Generate a token and return it to the client
app.post('/authenticate', (req, res) => {
    let grant_type = req.body.grant_type
    res.json(chatkit.authenticate({grant_type}, req.query.user_id))
});

// Index
app.get('/', (req, res) => { res.json("It works!") });

// 404!
app.use((req, res, next) => {
    let err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// Run it!
app.listen(4000, function(){
    console.log('App listening on port 4000!')
});

In the Express application above, we create the /login and /authenticate routes. The login route is responsible for creating a new user tied to our Chatkit application, while authenticate is responsible for getting a token for the user to make requests.

Finally, create a config.js file in the same root directory, this will be where we will define the Chatkit secret key and instance locator. Paste the contents below into the file:

module.exports = {
  instanceLocator: "PUSHER_CHATKIT_INSTANCE_LOCATOR",
  key: "PUSHER_CHATKIT_KEY",
}

⚠️ Remember to replace **PUSHER_CHATKIT_INSTANCE_LOCATOR** and **PUSHER_CHATKIT_KEY** with the actual values for your Chatkit application. You can find the values in the “Keys” section of the Chatkit dashboard.

Now we are done creating the Node.js application. Run the command below to start the Node.js application:

$ node index.js

‘One Last Thing’ – Configuring iOS to Allow Arbitrary Connections

If you still have your local Node.js web server running, you will need to make some changes so your application can talk to the local web server. In the info.plist file, make the following changes:

With this change, you can build and run your application and it will talk directly with your local web application. Now you can run your application:

Conclusion

In this article, we have been able to harness the power of the Pusher Chatkit SDK to build a messaging system with just the built-in UITableViewController. Hope you picked up a few things. I cannot wait to see what you build using Chatkit.

If you have any questions or feedback, let me know down in the comments. The source code is available on GitHub.

About Chris Nwamba

Chris is a JavaScript preacher. He also strives to make something out of other languages. Tech Writer. Dev Evangelist. Speaker.