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

Create an iOS chat app with Vapor - Part 2: Creating our iOS application

  • Christopher Batin
January 23rd, 2019
You will need Xcode 10 and Vapor 3.0. Some understanding of Vapor will be helpful.

Introduction

In this part of the tutorial, we will create an iOS app the interacts with the Vapor server that we created in the first part. The app will allow a user to log in or create a new user by just entering a username without authentication. We will create a chat app that has one room that all users are in and can chat with each other through.

Prerequisites

  • Understanding of Vapor - Please complete:
  • Xcode 10+
  • MacOS
  • Vapor 3.0 - Install instructions here.
  • An iOS device for testing notifications.
  • An understanding of iOS development and Xcode environment.
  • Cocoapods - Install instructions here.

Setup

Create our Xcode project

Open Xcode and create a new “**Single View App**” and call it “SuperChat”. We need to add our dependencies and to do this we will be using Cocoapods. Close Xcode and open the terminal and change directory to our new project and enter the following command.

    $ pod init

Open the newly created Podfile and add the following two pods:

      pod 'PushNotifications'
      pod 'PusherChatkit'
      pod 'MessageKit'
      pod 'NotificationBannerSwift'
      pod 'Alamofire'

Run the following command in the terminal:

    $ pod install

Now open the newly created Xcode workspace.

Set up the storyboard

Setup your Main.storyboard to look like the above image. The LoginViewController has a segue to the RoomsTableViewController with the identifier “toRooms”. The RoomsTableViewController has a segue to the RoomViewController with the identifier “toChatRoom”.

Handling login

Create a new Swift file called LoginViewController and add the following code to it. Return to your Storyboard and assign the class LoginViewController to the ViewController as shown in the above image. You can then use the assistant editor to hook up the outlet and actions for the text field and two buttons.

    // LoginViewController.swift
    import UIKit
    import Alamofire
    import PusherChatkit
    import NotificationBannerSwift

    class LoginViewController: UIViewController {

        @IBOutlet weak var usernameField: UITextField!

        struct CurrentUser: Codable {
            var id: String
            var name: String
        }
        var currentUser: CurrentUser?

        @IBAction func loginButtonTapped(_ sender: Any) {
            signInUser()
        }

        @IBAction func createUserButtonTapped(_ sender: Any) {
            createNewUser()
        }

        private func createNewUser() {
            guard let username = usernameField.text else {
                let banner = StatusBarNotificationBanner(title: "You need to provide a user name!", style: .danger)
                banner.show()
                return
            }
            let parameters: Parameters = [
                "name": username
            ]
            Alamofire.request(Constants.createUserURL, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON { (response) in
                let statusCode = response.response?.statusCode ?? -1
                if 200 ... 299 ~= statusCode {
                    if let data = response.data {
                        do {
                            let decoder = JSONDecoder.init()
                            self.currentUser = try decoder.decode(CurrentUser.self, from: data)
                            self.performSegue(withIdentifier: "toRooms", sender: self)
                        } catch {}
                    }
                } else {
                    let banner = StatusBarNotificationBanner(title: "Something went wrong, this user may already exist!", style: .danger)
                    banner.show()
                }
            }
        }

        private func signInUser() {
            guard let username = usernameField.text else {
                let banner = StatusBarNotificationBanner(title: "You need to provide a user name!", style: .danger)
                banner.show()
                return
            }
            let parameters: Parameters = [
                "name": username
            ]
            Alamofire.request(Constants.loginURL, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON { (response) in
                let statusCode = response.response?.statusCode ?? -1
                if 200 ... 299 ~= statusCode {
                    if let data = response.data {
                        do {
                            let decoder = JSONDecoder.init()
                            self.currentUser = try decoder.decode(CurrentUser.self, from: data)
                            self.performSegue(withIdentifier: "toRooms", sender: self)
                        } catch {}
                    }
                }else {
                    let banner = StatusBarNotificationBanner(title: "Something went wrong, this user may not exist yet!", style: .danger)
                    banner.show()
                }
            }
        }

        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "toRooms" {
                let vc = segue.destination as? RoomsTableViewController
                vc?.currentUserId = currentUser?.id
            }
        }
    }

There are two primary functions within the login view controller. Both functions require the username from the text field and encode it as a parameter. The createNewUser function will call our Vapor server and attempt to create a new user with the name provided if one doesn’t exist already. If this fails it will show an error. The signInUser function will call our Vapor server and attempt to find a user with that name. Both functions will return a User type that we will decode as we will need the users ID to interact with the ChatKit SDK.

Whilst we’re here we will add our constants file. Create a new file Constants.swift and add the following:

    // Constants.swift
    import Foundation
    import PusherChatkit

    let url = "http://localhost:8080"
    struct Constants {
        static let createUserURL = URL.init(string: "\(url)/api/users/new")!
        static let loginURL = URL.init(string: "\(url)/api/users/login")!
        static let tokenProvider = PCTokenProvider.init(url: "YOUR_TEST_TOKEN_ENDPOINT")
        static let chatkitInstance = "YOUR_CHATKIT_INSTANCE_ID"
    }

Notice how we are defining a token provider and the URL needs to be your test token endpoint that you enabled in your Chatkit dashboard.

Create and display rooms

Now we need a view that will show all the rooms that a user is in. Let’s create a new file called RoomsTableViewController.swift and add our code:

    //  RoomsTableViewController.swift
    import UIKit
    import PusherChatkit
    class RoomsTableViewController: UITableViewController {
       //1
        var chatManager: ChatManager!
        var currentUser: PCCurrentUser?
        var currentUserId: String!
        var rooms: [PCRoom]? {
            didSet {
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }
        var selectedRoom: PCRoom?
        //2
        override func viewDidLoad() {
            super.viewDidLoad()
            chatManager = ChatManager.init(instanceLocator: Constants.chatkitInstance,
                                                  tokenProvider: Constants.tokenProvider,
                                                  userID: currentUserId)
            chatManager.connect(delegate: self) { [unowned self] currentUser, error in
                guard error == nil else {
                    print("Error connecting: \(error!.localizedDescription)")
                    return
                }
                print("Connected!")

                guard let currentUser = currentUser else { return }
                self.currentUser = currentUser
                self.getRooms()
            }
        }
        //3
        private func getRooms() {
            // Must be new user try join general
            if currentUser?.rooms.count != 0 {
                self.rooms = currentUser?.rooms
            } else {
                getJoinableRooms()
            }
        }
        //4
        private func getJoinableRooms() {
            self.currentUser?.getJoinableRooms(completionHandler: { (userRooms, error) in
                //No rooms to join lets create one for everyone!
                if userRooms?.count == 0 {
                    self.createNewRoom()
                } else {
                    // Lets join the general chat (our only room)
                    for room in userRooms! where room.name == "general" {
                        self.currentUser?.joinRoom(room, completionHandler: { (room, error) in
                            if error == nil {
                                self.getRooms()
                            }
                        })
                    }
                }
            })
        }

        //Create a new public general room
        //5
        private func createNewRoom() {
            self.currentUser?.createRoom(name: "general", completionHandler: { (room, error) in
                self.getRooms()
            })
        }

        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "toChatRoom" {
                let vc = segue.destination as? RoomViewController
                vc?.currentRoom = selectedRoom
                vc?.currentUser = currentUser
            }
        }
    }
  • 1 - Here we define our variables for class. Importantly the user ID of our user we got from our login call in the login view controller.
  • 2 - In viewDidLoad we initialize our Chatkit manager using our user ID, token provider and chatkit instance ID. We then connect to the instance which returns us a PCCurrentUser, where most of our actions will take place.
  • 3 - Once we have connected to Chatkit we see if the user is in any rooms and assign them to our variable.
  • 4 - If the user is in no rooms we get all the rooms our user can join and join any rooms with the name general. If there are no rooms we can join we call a function to create one.
  • 5 - This function creates a room called “general” for everybody to join.

Now that we have our Chatkit and rooms creation logic setup we need to display this to users. Add the following code to your RoomsTableViewController. When the user selects a row we will pass that room to our RoomViewController.swift.

    // RoomsTableViewController.swift
    extension RoomsTableViewController: PCChatManagerDelegate {}

    extension RoomsTableViewController {
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }

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

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "RoomCell") as? RoomCell,
                let room = rooms?[indexPath.row] else {
                return UITableViewCell.init(frame: CGRect.zero)
            }
            cell.roomNameLabel.text = room.name
            return cell
        }

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

We now need to create our custom cell class. Create a new file called RoomCell.swift and add the following:

    class RoomCell: UITableViewCell {
        @IBOutlet weak var roomNameLabel: UILabel!
    }

Open your Main.storyboard, and assign your TableViewController to your new RoomsTableViewController through the inspector. Select the cell on the table view and assign the custom class RoomCell and connect your outlet. You also need to give your cell the unique identifier “RoomCell”.

Interacting in a room

Our final task is to interact within the room. Create a new file named RoomViewController.swift and add:

    // RoomViewController.swift
    import Foundation
    import UIKit
    import MessageKit
    import MessageInputBar
    import PusherChatkit
    import NotificationBannerSwift

    class RoomViewController: MessagesViewController, PCRoomDelegate {
        //1
        var messages: [Message] = []
        var room: [String: Any] = [:]
        var currentRoom: PCRoom? = nil
        var currentUser: PCCurrentUser? = nil

        var chatManager: ChatManager?
        //2 
        override func viewDidLoad() {
            super.viewDidLoad()

            configureMessageKit()

            navigationItem.title = currentRoom?.name

            chatManager = ChatManager.init(instanceLocator: Constants.chatkitInstance,
                                           tokenProvider: Constants.tokenProvider,
                                           userID: currentUser!.id)

            currentUser?.subscribeToRoom(room: currentRoom!, roomDelegate: self, completionHandler: { (error) in
                if error != nil {
                    print(error as Any)
                }
                self.fetchMessages()
            })
        }

        func configureMessageKit() {
            messagesCollectionView.messagesDataSource = self
            messagesCollectionView.messagesLayoutDelegate = self
            messagesCollectionView.messagesDisplayDelegate = self

            // Input bar
            messageInputBar = MessageInputBar()
            messageInputBar.sendButton.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1)
            messageInputBar.delegate = self
            messageInputBar.backgroundView.backgroundColor = .white
            messageInputBar.isTranslucent = false
            messageInputBar.inputTextView.backgroundColor = UIColor(red: 249/255, green: 250/255, blue: 252/255, alpha: 1)
            messageInputBar.inputTextView.layer.borderColor = UIColor(red: 192/255, green: 204/255, blue: 218/255, alpha: 1).cgColor
            messageInputBar.inputTextView.layer.borderWidth = 0
            reloadInputViews()

            // Keyboard and send btn
            messageInputBar.sendButton.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1)
            scrollsToBottomOnKeyboardBeginsEditing = true
            maintainPositionOnKeyboardFrameChanged = true
        }

        // 3
        func onMessage(_ message: PCMessage) {
            let msg = Message(
                text: message.text,
                sender: Sender(id: message.sender.id, displayName: message.sender.displayName),
                messageId: String(describing: message.id),
                date: ISO8601DateFormatter().date(from: message.createdAt)!
            )

            DispatchQueue.main.async {
                self.messages.append(msg)
                self.messagesCollectionView.reloadData()
                self.messagesCollectionView.scrollToBottom()
            }
        }

        //4
        func fetchMessages() {
            currentUser?.fetchMessagesFromRoom(currentRoom!, completionHandler: { (roomMessages, error) in
                if error != nil {
                    print(error as Any)
                }
                guard let roomMessages = roomMessages else { return }
                DispatchQueue.main.async {
                    for message in roomMessages {
                        let sender = Sender.init(id: message.sender.id,
                                                 displayName: message.sender.displayName)
                        self.messages.append(Message.init(text: message.text ,
                                                          sender: sender,
                                                          messageId: String(message.id),
                                                          date: message.createdAtDate))
                    }
                    self.messagesCollectionView.reloadData()
                }
            })
        }
    }
  • 1 - Our variables for this class, including the room we pass from our previous view controller and an array of messages.
  • 2 - We reinitializes our chat manager, although it is worth noting we could also pass this between view controllers. However more importantly we subscribe the user to the room so that they may receive updates, e.g messages.
  • 3 - A delegate method on PCRoomDelegate for when we receive a message. We create a new Message object and then append it to our array of messages. We then reload our messages collection view.

This class subclasses MessagesViewController which is an open source project to display messages in a format like iMessage. The project can be found here. We need to add some more logic to support this. Add the following code below your RoomViewController:

    extension RoomViewController: MessagesDisplayDelegate {
        func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
            return isFromCurrentSender(message: message) ? .white : .darkText
        }

        func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
            return isFromCurrentSender(message: message)
                ? UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1)
                : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
        }

        func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
            let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
            return .bubbleTail(corner, .curved)
        }
    }

    extension RoomViewController: MessageInputBarDelegate {
        func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
            guard let room = currentRoom else { return }

            currentUser?.sendMessage(roomID: room.id, text: text) { msgId, error in
                if error == nil {
                    DispatchQueue.main.async { inputBar.inputTextView.text = String() }
                }
            }
        }
    }

    extension RoomViewController: MessagesLayoutDelegate {
        func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
            return 16
        }

        func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
            return 16
        }

        func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition {
            return AvatarPosition(horizontal: .natural, vertical: .messageBottom)
        }

        func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
            let stringFromCharacter = String(message.sender.displayName.first ?? "?")
            avatarView.set(avatar: Avatar.init(image: nil, initials: stringFromCharacter))
        }

        func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
            return isFromCurrentSender(message: message)
                ? UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
                : UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)
        }

        func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
            return CGSize(width: messagesCollectionView.bounds.width, height: 10)
        }

        func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
            return 200
        }
    }


    extension RoomViewController: MessagesDataSource {
        func isFromCurrentSender(message: MessageType) -> Bool {
            return message.sender == currentSender()
        }

        func currentSender() -> Sender {
            return Sender(id: currentUser!.id, displayName: (currentUser!.name)!)
        }

        func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
            return self.messages[indexPath.section]
        }

        func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
            return self.messages.count
        }

    }

The important sections here are at the bottom where we are checking the number of messages and also who the messages are from by comparing the current user’s ID against the that of the sender of the message. Remember to set the RoomViewController custom class in your storyboard.

Finally you will notice that we have build errors. We need to create a new struct like below. You can do this in a new file called Message.swift.

    // Message.swift
    import Foundation
    import MessageKit

    struct Message: MessageType {
        var messageId: String
        var sender: Sender
        var sentDate: Date
        var kind: MessageKind

        init(kind: MessageKind, sender: Sender, messageId: String, date: Date) {
            self.kind = kind
            self.sender = sender
            self.messageId = messageId
            self.sentDate = date
        }

        init(text: String, sender: Sender, messageId: String, date: Date) {
            self.init(kind: .text(text), sender: sender, messageId: messageId, date: date)
        }
    }

Conclusion

In this part of the tutorial we’ve learnt how to integrate with the Pusher Chatkit SDK but also with the Vapor server we created in part one. We now have a working chat app, you could customize this example to add more rooms or password authentication on our server.

The source code for this example can be found here.

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.