🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Building a chat app with emoji reactions - Part 3: Building the iOS app

  • Neo Ighodaro

June 18th, 2019
For this part of the series, you will need Xcode 9+ installed on your machine.

Introduction

This is the final part of our three-part series. In the previous part, we completed the Android application for our chat app.

In this final part, we will build an iOS that replicates what we have in the Android app. When we finish building, our app will look like this:

Prerequisites

Before going ahead, be sure to meet the following requirements:

  • Completed the first part of the series. You don’t need to complete the second part to complete this part.
  • Have Xcode v9 or later installed on your machine. You can download it here.
  • Knowledge of the Swift programming language.
  • Knowledge of the Xcode IDE.

Creating a project

To start with, you need to create an iOS project. Open Xcode and create a new project:

Select Single View App and click Next. After that, enter the product name - ChatkitEmoji and other organization credentials.

Building our app

When your project is all ready, the first thing you will do is add dependencies. Create a pod file in the root directory of your project by running this command:

    $ pod init

This generates a Podfile file for you. Open the file and replace it with this:

    # File: ./Podfile
    # platform :ios, '9.0'

    target 'ChatkitEmoji' do
      use_frameworks!

      pod 'Alamofire'
      pod 'PusherChatkit'
      pod 'PusherSwift'
    end

After that, run this command to install your dependencies:

    $ pod install

As soon as this is completed, close your ChatkitEmoji.xcodeproj and open the ChatkitEmoji.xcworkspace project in Xcode.

Next, open the Info.plist file and add this:

    <!-- File: ./ChatkitEmojiTest/Info.plist -->
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <!-- [...] -->

      <key>NSAppTransportSecurity</key>
      <dict>
          <key>NSExceptionDomains</key>
          <dict>
              <key>localhost</key>
              <dict>
                  <key>NSExceptionAllowsInsecureHTTPLoads</key>
                  <true/>
                  <key>NSIncludesSubdomains</key>
                  <true/>
              </dict>
          </dict>
      </dict>

    </dict>
    </plist>

This is used to put an exception to the security policy which explicitly requires a secure connection.

After that, open the Main.storyboard file, you will see a view controller scene. We will modify the empty view and make it our login screen. Drag a text field element to the center of the view and add the following constraints to it:

This centers the text field horizontally and vertically.

This stretches the text field to match the parent and a margin of 10 on the left and right. We also set the height to 50.

Next, drag a button and drop it below the text input field. When dropping it, place it central horizontally and just below the text field. You will be shown some lines to guide you when trying to place it. After that, add missing constraints to the button like this:

Now, let us add logic to the ViewController.swift file. First rename the file to LoginViewController.swift for better clarity. After renaming it, go back to the Main.storyboard file, select the view controller and use the identity inspector to change the custom class to LoginViewController.

Now, open your LoginViewController file and add replace it with this:

    // File: ./ChatkitEmojiTest/LoginViewController.swift
    import UIKit
    import Alamofire
    import PusherChatkit

    class LoginViewController: UIViewController {

        var chatManager: ChatManager!
        var currentUser: PCCurrentUser?
        var chatManagerDelegate: PCChatManagerDelegate?

        @IBOutlet weak var userTextField: UITextField!

        @IBAction func signIn(_ sender: Any) {
            if(!((userTextField.text?.isEmpty)!)){

                Alamofire.request("http://localhost:3000/users",
                                  method: .post,
                                  parameters: ["userId": userTextField.text!], encoding: JSONEncoding.default)
                    .validate()
                    .responseJSON { response in
                        if(response.response!.statusCode==200) {
                            self.setupChatManager()
                        }
                }
            }
        }
    }

    class MyChatManagerDelegate: PCChatManagerDelegate {}

This class has an action attached to the sign in button. This is found in the signIn function. In the function, we make a network call to create a new user using the username inputted by the user. When it is successful, we call another function to setupChatManager. Connect the required @IBAction and @IBOutlet.

Next, create the function setupChatManager inside the LoginViewController class like so:

    func setupChatManager() {
            self.chatManager = ChatManager(
                instanceLocator: "YOUR_CHATKIT_INSTANCE_LOCATOR",
                tokenProvider: PCTokenProvider(url: "http://localhost:3000/token"),
                userID: self.userTextField.text!
            )

            self.chatManagerDelegate = MyChatManagerDelegate()

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

                guard let currentUser = currentUser else {
                    print("CurrentUser object is nil")
                    return
                }
                self.currentUser = currentUser

                DispatchQueue.main.async() {
                    let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
                    let newViewController = storyBoard.instantiateViewController(withIdentifier:"chatRoomViewController")
                        as! ChatRoomViewController

                    newViewController.currentUser = currentUser

                    let navigationController = UINavigationController(rootViewController: newViewController)
                    self.present(navigationController, animated: true, completion: nil)

                }

            }
        }

Replace YOUR_CHATKIT_INSTANCE_LOCATOR with the key on your dashboard.

In this snippet, we initialize the chat manager with our Chatkit keys and connect the chat manager. After the connection is successful, we open the next view controller and pass the currentUser value to it. Let us design and implement the next view, our chat room.

Open your Main.storyboard, drag another table view controller to the board. Set the storyboard ID of the table view controller scene to chatRoomViewController. Then, select the table view cell, set the reuse identifier to messageCell like this:

Now, increase the height of your table cell, after that, drag three labels to the table view cell, one under the other. For the second one, set the font to 15 and the font to a darker gray.

For the labels, add left and right constraints to them with a spacing of 20 each. Also, add constraints to the top with a spacing of 5.

Next, we need a model class for the data we will populate on the table view. Create a new swift file named MessageModel and paste this in the file:

    // File: ./ChatkitEmojiTest/MessageModel.swift
    import PusherChatkit

    class MessageModel {
        var message:PCMessage!
        var emojiModelList = [EmojiModel]()
    }

    class EmojiModel {
        var string:String?
        var count: Int?
        var usersToEmoji: [String]?
    }

After that, we need a custom cell class for our table view. Create a class named MessageCell and paste this:

    // File: ./ChatkitEmojiTest/MessageCell.swift
    import UIKit
    import PusherChatkit

    class MessageCell : UITableViewCell {
        @IBOutlet weak var message: UILabel!
        @IBOutlet weak var username: UILabel!
        @IBOutlet weak var selectedEmojis: UILabel!

        func setItems(model:MessageModel){        
            var emojiString = ""

            for item in model.emojiModelList {
                emojiString = emojiString + item.string! + String(item.count!) + " "
            }

            selectedEmojis.text = emojiString
            message.text = model.message.text
            username.text = model.message.sender.id
        }
    }

We have to set the custom class for our table view cell from the Main.storyboard file. Change it from the default UITableViewCell to MessageCell. After that link the outlets from your storyboard to the MessageCell class.

Now, let us create a view controller for the chat room screen. Create a new class swift file called ChatRoomViewController. Replace the contents of the file with this:

    // File: ./ChatkitEmojiTest/ChatRoomViewController.swift
    import UIKit
    import Foundation
    import PusherChatkit
    import Alamofire
    import PusherSwift

    class ChatRoomViewController: UITableViewController {
        var currentUser: PCCurrentUser?
        var messageList: [MessageModel] = []
        var currentRow = -1
        var options:PusherClientOptions?
        var pusher: Pusher?

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

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

        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            currentRow = indexPath.row
            self.performSegue(withIdentifier: "openEmojiList", sender: self)
        }

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "messageCell", for: indexPath) as! MessageCell
            cell.setItems(model: messageList[indexPath.row])
            return cell
        }

        override func viewDidLoad() {
            super.viewDidLoad()
            navigationController?.navigationBar.prefersLargeTitles = true
            subscribeToRoom()
            setupPusher()
        }
    }

    extension ChatRoomViewController: PCRoomDelegate {
        func onMessage(_ message: PCMessage) {
            let item = MessageModel()
            item.message = message
            self.messageList.insert(item, at:0)
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }

Here, we are implementing some table view functions that come with the UITableViewController. In order of which they are arranged; we have a function that returns the height of each row, the size of the table view handles selection on each row and binds data to each row.

In the Main.storyboard file set the custom class for the table view controller scene to ChatRoomViewController.

We have a class extension here too which handles when we receive new messages. Inside the viewDidLoad function, we call two functions:

  • subscribeToRoom: This function subscribes the user to the first room he has joined already. Create the function inside the view controller class like so:
    func subscribeToRoom() {
        currentUser!.subscribeToRoom(room: currentUser!.rooms[0], roomDelegate: self) { error in
            guard error == nil else {
                print("Error subscribing to room: \(error!.localizedDescription)")
                return
            }

            print("Subscribed to room!")
        }
    }
  • setupPusher - This function initializes Pusher Channels with the keys from our Channels dashboard. Create the function inside the class like so:
    func setupPusher(){
        options = PusherClientOptions(host: .cluster("PUSHER_APP_CLUSTER"))

        pusher = Pusher(
          key: "PUSHER_APP_KEY",
          options: self.options!
        )

        pusher!.connect()
        pusherSubscribe()
    }

Replace the PUSHER_* keys with the one in your dashboard.

After connecting to Pusher, we call a custom pusherSubscribe function that we use to get updates about the emoji reactions for the messages.

Add the function inside your ChatRoomViewController file like so:

    func pusherSubscribe(){
      let channel = pusher!.subscribe((self.currentUser?.rooms[0].id)!)

      let _ = channel.bind(eventName: "emoji-event", callback: { (data: Any?) -> Void in
          if let data = data as? [String : AnyObject] {

              for message in self.messageList {
                  if (String(message.message.id) == String((data["messageId"] as? Int)!)){
                      var doesEmojiExist = false

                      for (index, emojiItem) in message.emojiModelList.enumerated() {
                          if (data["emoji"] as? String == emojiItem.string){
                              doesEmojiExist = true
                              if( (data["count"] as? Int) == 0 ){
                                  message.emojiModelList.remove(at: index)
                              } else {
                                  emojiItem.count = data["count"] as? Int
                              }
                          }
                      }

                      if (!doesEmojiExist) {
                          let newModel = EmojiModel()
                          newModel.string = data["emoji"] as? String
                          newModel.count = data["count"] as? Int
                          newModel.usersToEmoji = String((data["userIds"]! as? String)!).toJSON() as? [String]

                          message.emojiModelList.append(newModel)
                      }
                  }
              }

              self.tableView.reloadData()
          }
      })
    }

In this function, we check the data coming from Channels and get the message ID to be updated. We then loop through the message list to know which item matches. Next, we try to check if the emoji sent from the server already exists, if it does we just update it else, we add a new one.

We used a toJSON extension for strings. Add the extension outside the class like so:

    extension String {
        func toJSON() -> Any? {
            guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
            return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
        }
    }

Now, let us link this our view to the next view. So that when a user selects a row on the table view, we can open a list of available emojis for them to add.

Open your Main.storyboard file, drag a new collection view controller and drop there. Add a Show Detail (Replace) segue from messageCell of the chat view controller scene to the new controller we just added. Make sure the segue isn’t a cell selection selection segue.

Select the segue and change the identifier to openEmojiList through the identity inspector. After that, add a label to your collection view cell. Place the label centrally. Then change the identifier for the collection view cell to emojiCell.

Next, we will create a class for the collection view cell. Create a new swift file called EmojiCell and paste this:

    // File: ./ChatkitEmojiTest/EmojiCell.swift
    import UIKit

    class EmojiCell: UICollectionViewCell {
        @IBOutlet weak var emojiText: UILabel!
    }

Select your collection view cell and set the custom class to EmojiCell after which you link your emojiText outlet from the collection view cell to this class.

Now, create a new class named EmojiListViewController and paste this in the file:

    // File: ./ChatkitEmojiTest/EmojiListViewController.swift
    import UIKit

    class EmojiListViewController: UICollectionViewController {
        var callback : ((String)->())?
        var emojiList = [String]()

        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return emojiList.count
        }

        override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            callback?(emojiList[indexPath.row])
            self.dismiss(animated: true, completion: nil)
        }

        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "emojiCell", for: indexPath) as! EmojiCell

            cell.emojiText.text = emojiList[indexPath.row]
            return cell
        }

        override func viewDidLoad() {
            super.viewDidLoad()
            emojiList.append("😉")
            emojiList.append("😡")
            emojiList.append("👺")
            emojiList.append("🥰")
            emojiList.append("😡")

            self.collectionView.reloadData()
        }
    }

In this class, we set up the collection view and add some emojis the user can use. When an emoji is selected, we close this view and send back result to the chat room. Go to your storyboard and select the custom class for the collection view controller to EmojiListViewController.

Next, add this function to the class:

    // File: ./ChatkitEmojiTest/EmojiListViewController.swift
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "openEmojiList" {
            let emojiListViewController = segue.destination as! EmojiListViewController
            emojiListViewController.callback = { result in
                var ifEmojiExists = false;

                for emoji in self.messageList[self.currentRow].emojiModelList {
                    if(emoji.string==result){
                        ifEmojiExists = true
                        if(!(emoji.usersToEmoji!.contains((self.currentUser?.id)!))){
                            emoji.usersToEmoji!.append((self.currentUser?.id)!)
                            let newCount = emoji.count! + 1
                            self.sendToPusher(emojiString: emoji.string!, count: newCount, users: emoji.usersToEmoji!)

                        } else {
                            emoji.usersToEmoji!.removeAll { $0 == self.currentUser?.id }

                            if (emoji.count! > 0) {
                                emoji.count = emoji.count! - 1
                            } else {
                                emoji.count = 0
                            }

                            self.sendToPusher(emojiString: result, count: emoji.count!, users: emoji.usersToEmoji!)
                        }
                    }
                }

                if (!ifEmojiExists) {
                    var userIds = [String]()
                    userIds.append(self.currentUser!.id)
                    self.sendToPusher(emojiString: result, count: 1, users: userIds)
                }

                self.tableView.reloadData()
            }
        }
    }

Here, we wait for a result from the EmojiListViewController, we check to know if the current user had added it before or not. If it did, we update the count and tell out server using the sendToPusher function. Create the function like this:

    // File: ./ChatkitEmojiTest/EmojiListViewController.swift
    func sendToPusher(emojiString: String, count:Int, users:[String]){
      Alamofire
        .request("http://localhost:3000/updateEmoji",
          method: .post,
          parameters: [
            "messageId": messageList[currentRow].message.id, 
            "roomId": self.currentUser!.rooms[0].id, 
            "emoji": emojiString, 
            "count":count,
            "userIds": toJsonString(from: users) as Any
          ], 
          encoding: JSONEncoding.default
        )
        .validate()
        .responseJSON { response in
            if(response.response!.statusCode==200) {
            }
        }
    }

We also made use of another function toJsonString to convert the users in the array to JSON. Create the function like this:

    // File: ./ChatkitEmojiTest/EmojiListViewController.swift
    func toJsonString(from object:Any) -> String? {
        guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
            return nil
        }

        return String(data: data, encoding: String.Encoding.utf8)
    }

Now, when we run our app, we have something like this:

Conclusion

You’ve successfully come to the end of this part and the series in general. Here, we were able to build an iOS app that subscribes to a room and adds emoji reactions to messages. Feel free to build on top of this and achieve something great.

Here is the GitHub repo to the application built in this series.

Clone the project repository
  • Android
  • Chat
  • iOS
  • JavaScript
  • Kotlin
  • Node.js
  • Swift
  • Channels
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 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.