Adding chat to your app can be a quick way of going from having just another mobile app to an app that allows users to connect with one another. Whether your app is about connecting employers with job hunters, sellers with buyers, daters with one another, or something else entirely - your users will surely apprecaite the easier and more direct communication.

Making an iOS chat app is easy! Here’s how.

In this iOS chat tutorial, you’ll learn how to show a beautiful chat interface in your app, manage the keyboard, show messages in a table view and send and receive messages to and from the Internet. In other words, you’ll go from nothing to having a working chat app in a short time.

If you’d like to jump straight to the finished project, you can see it on GitHub.

Before we start sending and receiving messages, we first need to start building out the chat UI. The main chat UI will be a table with that shows chat messages. You’ll show two types of cells: one for incoming, and one for outgoing messages.

Hold your horses ⚠️🐴!
To follow this tutorial or run the example source code you'll need to create a V1 application.

v2 will be out of beta soon at which point we will update this tutorial.

Showing messages

To hit the ground running, download an Xcode project that contains some of the UI and boilerplate required for this tutorial, already built for you. You can get it by going to the start-here branch of the GitHub project. Download or clone the project anywhere on your system.

Here’s what you’ll find in the project:

  • Incoming and outgoing table view cells are already created for you, with their required model structs.
  • A skeleton chat view controller with an empty table view — which you’ll fill up shortly — and a text view.
  • A skeleton login view controller that leads to the chat interface — again, you’ll implement logging in later in the tutorial.
  • The CometChat Pro SDK, already added as a dependency to the project.

The starting project saves you from doing grunt-work and lets you focus on the parts of building an iOS chat app that really matter.

Let’s get familiar with the starting project. Open the .workspace file and take a look. It contains a couple of view controllers, and the main one that you’ll be changing is ChatViewController.swift. Like a lot of things in iOS, a chat app is just a fancy table view.

There are also two models to get you started, User.swift and Message.swift. These are two structs with some basic information that you’ll present on the screen. Let’s get started on that.

Open up ChatViewController.swift and change the messages declaration to the following:

var messages: [Message] = [
    Message(
        user: User(name: "Jamie"),
        content: "Hey, did you see that cool chat tutorial?",
        isIncoming: true),
    Message(
        user: User(name: "Sandra"),
        content: "Hey! No, where is it?",
        isIncoming: false),
    Message(
        user: User(name: "Jamie"),
        content: "It's on CometChat's blog!",
        isIncoming: true),
]

This creates a couple of fake chat messages for display in your UI. Later, we’ll remove these and fetch messages from CometChat.

Next, implement UITableViewDataSource. Add the following extension to the end of the file:

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

The above code tells the table view how many cells there are.

Now, implement the second required method inside the extension, right below the one you just wrote:

func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  
  let message = messages[indexPath.row]
  let cellIdentifier = message.isIncoming ?
    Constants.incomingMessageCell :
    Constants.outgoingMessageCell
  
  guard let cell = tableView.dequeueReusableCell(
    withIdentifier: cellIdentifier, for: indexPath)
    as? MessageCell & UITableViewCell else {
    return UITableViewCell()
  }
  
  cell.message = message
  return cell
}

First, grab the message for the cell’s index. There are two different UITableViewCells in the provided materials, for incoming and outgoing messages, respectively. Both of these implement a protocol called MessageCell, which has properties you set to populate the cell with data.

You decide between these two cells by changing the cell identifier based on whether or not the message is incoming. Once you have that identifier, you can dequeue a new cell instance. The cast to MessageCell & UITableViewCell says “make sure this is a table view cell that implements the MessageCell protocol”.

Finally, add the message to the cell and return it.

One more step before we can show the cells is to assign the view controller as the table view’s data source. Add the following line to the end of viewDidLoad:

tableView.dataSource = self

If you run the project at this point, you should see the messages in your chat screen.

Your first chat messages!

And that’s our chat app done! Let’s pack it up… Just kidding. There’s lots more to do! For instance, tapping the text view brings up a keyboard as it should — but it also hides the text field and half of our view controller!

The keyboard appears over the text view and the table view. We can’t have that.

Let’s take care of that next.

Dealing with the keyboard

Keyboard issues like this are common whenever you have a text field and a scroll view together. It’s hard to have a chat app without a keyboard, so learning how to deal with these issues is a must. Here’s how to deal with these issues:

One way to fix these keyboard issues is to track when the keyboard pops up. Once it does, we can move the text view to always stay on top of the keyboard. Since we’re using auto layout, if we move the text view the table view it will follow suit.

Let’s start by adding the following method call to the end of viewDidLoad:

startObservingKeyboard()

Then, add that method to the class, below viewDidLoad:

private func startObservingKeyboard() {
  let notificationCenter = NotificationCenter.default
  notificationCenter.addObserver(
    forName: UIResponder.keyboardWillShowNotification,
    object: nil,
    queue: nil,
    using: keyboardWillAppear)
  notificationCenter.addObserver(
    forName: UIResponder.keyboardWillHideNotification,
    object: nil,
    queue: nil,
    using: keyboardWillDisappear)
}

Thankfully, the iOS keyboard sends NotificationCenter notifications that we can observe and react to. We’ll add two new observers for when the keyboard is shown and hidden. These will call keyboardWillAppear and keyboardWillDisappear, which we’ll implement in a second.

Before we do that, we need to make sure we have good memory hygiene. Add the following to the class:

deinit {
  let notificationCenter = NotificationCenter.default
  notificationCenter.removeObserver(
    self,
    name: UIResponder.keyboardWillShowNotification,
    object: nil)
  notificationCenter.removeObserver(
    self,
    name: UIResponder.keyboardWillHideNotification,
    object: nil)
}

This makes sure that, if the view controller ever gets deinitialized, it will remove itself as an observer from these two notifications. The reason why we do this is because NotificationCenter keeps a strong reference to its observers. If we don’t manually remove the observer, it will never get destroyed and will result in a memory leak.

Now, we can implement those two methods for handling the notifications. Start by telling the view controller what to do when the keyboard appears. Add this method to the class:

private func keyboardWillAppear(_ notification: Notification) {
  let key = UIResponder.keyboardFrameEndUserInfoKey
  guard let keyboardFrame = notification.userInfo?[key] as? CGRect else {
    return
  }
  
  let safeAreaBottom = view.safeAreaLayoutGuide.layoutFrame.maxY
  let viewHeight = view.bounds.height
  let safeAreaOffset = viewHeight - safeAreaBottom
  
  let lastVisibleCell = tableView.indexPathsForVisibleRows?.last

}

First, grab the keyboard’s frame so you can get its height. Notifications can pass data trough a dictionary called userInfo, which is where you get the keyboard’s frame.

Once you have the frame, you can do some math to figure out the distance between the bottom of your view and the text view. This is the safe area padding that is useful when the text view is at the bottom of the screen, but once it’s on top of the keyboard you’ll need to remove that padding, otherwise, it will look like it’s hovering above the keyboard.

Also, grab the last cell’s index path so that you can scroll to it once the keyboard appears.

Next, add the following code to the bottom of the method:

UIView.animate(
  withDuration: 0.3,
  delay: 0,
  options: [.curveEaseInOut],
  animations: {
    self.textAreaBottom.constant = -keyboardFrame.height + safeAreaOffset
    self.view.layoutIfNeeded()
    if let lastVisibleCell = lastVisibleCell {
      self.tableView.scrollToRow(
        at: lastVisibleCell,
        at: .bottom,
        animated: false)
    }
})

This creates a simple animation that moves the text view up by the height of the keyboard, minus the padding you calculated earlier. You’ll also tell the table view to scroll to the last cell if there is one.

Finally, do a similar thing when the keyboard disappears, only this time move everything to the bottom again. Add this method to the class:

private func keyboardWillDisappear(_ notification: Notification) {
  UIView.animate(
    withDuration: 0.3,
    delay: 0,
    options: [.curveEaseInOut],
    animations: {
      self.textAreaBottom.constant = 0
      self.view.layoutIfNeeded()
  })
}

This code is analogous to the one you wrote a second ago, except it moves the constraint back to zero.

Build and run the project now and you should see the text view follow the keyboard when it gets activated.

Now the text view rises with the keyboard, thanks to NotificationCenter!

Connecting to CometChat

We’ve come as far as we’ll go with fake messages. It’s time for the real thing!

For this chat app, we’ll use CometChat Pro. CometChat Pro provides chat SDKs and an API that makes it easy to send and receive messages, without needing to code a backend service and deal with web sockets in your app. It also supports sending images, group and 1-on-1 chats, typing indicators and more. The best part is that CometChat provides SDKs for iOS, Android and Javascript so you can easily chat between multiple platforms. And — it’s completely free to get started!

We’ll start by creating a new CometChat Pro app. Head over to CometChat registration and create a new account.

Once you’re in the CometChat Dashboard, create a new app and name it anything you like. Once created, click on Explore and then API Keys in the sidebar. Take note of the fullAccess API Key and app ID. You’ll need them in a bit.

Note down your API key and app ID from CometChat’s dashboard.

If you’re working from the GitHub project, the CometChat SDK is already installed and ready. If you’re building a chat app from scratch, add CometChat as a dependency as described here, and then come back to this tutorial. Don’t worry, we’ll wait.

Create a new Swift file and name it ChatService.swift. This file will contain a class that will manage everything related to CometChat: sending and receiving messages, fetching old messages, authenticating and joining groups.

Start by changing the contents of the file to the following:

import Foundation
import CometChatPro

final class ChatService {
  
  private enum Constants {
    static let cometChatAPIKey = "API_KEY"
    static let cometChatAppID = "APP_ID"
    static let groupID = "supergroup"
  }
  
  static let shared = ChatService()
  private init() {}

}

This declares a new class that you can make a singleton for the purpose of this tutorial. You also declare a couple of constants. Make sure to enter your API key and app ID in the respective fields. The supergroup group ID is created for you by default when you make a new CometChat app.

Next, add a static function to the class that initializes CometChat:

static func initialize() {
  CometChat(
    appId: Constants.cometChatAppID,
    onSuccess: { isSuccess in
      print("CometChat connected successfully: \(isSuccess)")
    },
    onError: { error in
      print(error)
  })
}

This starts up the CometChat SDK with the provided app ID. This function should be called as soon as the app launches. Head over to AppDelegate.swift and at the start of func application(_ application: UIApplication, didFinishLaunchingWithOptions, call initialize:

ChatService.initialize()

Back in ChatService.swift, now that you initialized CometChat SDK, you need a way for the user to join a chat session.

In order to receive messages in our chat app, two things need to happen. First, the user has to log in to CometChat. This tells CometChat what’s the ID of the user, so it knows which messages to send. The second thing that needs to happen is joining a group. Since we’re building a group chat, we will be sending all messages to a specific group ID. To receive those messages, our users need to join that group.

When you create a new CometChat app, it comes with a pre-created group with the ID supergroup. For this tutorial, you’ll join every logged in user to this group. In a production app, you might want to have multiple groups to achieve a feature similar to Slack channels. That’s why CometChat lets you create as many groups as you like.

First, add a new property to the class to store the currently logged-in user:

private var user: User?

Next, add the following method to the class:

func login(
  email: String, 
  onComplete: @escaping (Result<User, Error>)-> Void) {
  
  CometChat.login(
    UID: email,
    apiKey: Constants.cometChatAPIKey,
    onSuccess: { [weak self] user in
      guard let self = self else { return }
      self.user = User(name: email)
      self.joinGroup(as: self.user!)
      DispatchQueue.main.async {
        onComplete(.success(self.user!))
      }
    }, 
    onError: { error in
      print("Error logging in:")
      print(error.errorDescription)
      DispatchQueue.main.async {
        onComplete(.failure(NSError()))
      }
  })
}

This might be a chunky block of code, but it’s not as complicated as it looks. Let’s break it down piece by piece:

  1. The method takes two arguments, the user’s email — which you’ll use as a unique identifier — and a completion handler that can either be the logged-in user or an error.

  2. Call CometChat’s login function which takes a user ID and the API key you set earlier and has two completion handlers, one for success and another for an error. If the user logged in successfully, start joining a group. We’ll implement that in a second.

  3. Otherwise, if there was an error along the way, print it out and call the completion with a failed result. You’ll have to dispatch to the main thread here because CometChat works in the background, so it calls the completion handler on a background thread.

After logging in — as we discussed earlier — the user needs to join the supergroup group. Add the following method right after login:

private func joinGroup(as user: User) {
  CometChat.joinGroup(
    GUID: Constants.groupID,
    groupType: .public,
    password: nil,
    onSuccess: { _ in
      print("Group joined successfully!")
    },
    onError: { error in
      print("Error:", error?.errorDescription ?? "Unknown")
  })
}

This method is simple enough: Call CometChat’s joinGroup function and give it the group ID, then print out the result.

Now that we have all the pieces to log a user in, it’s time to put it all together in LoginViewController.swift. Change the contents of loginButtonTapped to the following:

let email = emailTextField.text!
guard !email.isEmpty else {
  return
}

ChatService.shared.login(email: email) { [weak self] result in
  DispatchQueue.main.async {
    switch result {
    case .success:
      self?.performSegue(withIdentifier: Constants.showChat, sender: nil)
    case .failure(let error):
      print(error)
      self?.showError("An error occurred.")
    }
  }
}

First, make sure the user entered their email. Then, call login and on success navigate to ChatViewController. If there was an error, present an alert.

Run the project and head to the Login screen. As the email, enter superhero1. From now on, that’s your email. superhero1 trough superhero5 are special UIDs that are created for you when you create a new CometChat app.

Log into the chat app with the user ID “superhero1”.

Tap Login and you should be taken to the chat screen. We haven’t implemented sending messages yet, so you’ll have to trust me that you are logged in.

Receiving messages

A chat app is not much use if you can only talk to yourself, so let’s add support for receiving messages from CometChat. Head back to ChatService.swift and add the following property to the class:

var onRecievedMessage: ((Message)-> Void)?

This is a callback that you will call whenever you get a new message. This is how the chat screen will know to display new messages.

In order to receive messages from CometChat, you need to implement CometChatMessageDelegate. Add the following extension to the bottom of the file:

extension ChatService: CometChatMessageDelegate {
  func onTextMessageReceived(textMessage: TextMessage) {
    DispatchQueue.main.async {
      self.onRecievedMessage?(Message(textMessage, isIncoming: true))
    }
  }
}

Here we route the delegate method to the callback we declared earlier. Make sure to dispatch to the main queue because the delegate method can be called from a background thread.

Next, set ChatService as the message delegate by adding the following line at the start of login:

CometChat.messagedelegate = self

Then, in ChatViewController.swift, connect the messages array with the callback by adding the following line to the end of viewDidLoad:

emptyChatView.isHidden = false
ChatService.shared.onRecievedMessage = { [weak self] message in
  self?.messages.append(message)
  self?.scrollToLastCell()
}

Finally, change the declaration of messages to the following:

var messages: [Message] = [] {
  didSet {
    emptyChatView.isHidden = !messages.isEmpty
    tableView.reloadData()
  }
}

This removes the fake messages we had earlier and adds a property observer that will reload the table view and hide or show the “no messages” view as needed.

Feel free to run the project now, but you won’t get far — while you can receive messages, you haven’t yet added support for sending them. Let’s do that next.

Sending messages

Back in ChatService.swift, add a method to send a text message:

func send(message: String) {
  guard let user = user else {
    return
  }
  
  let textMessage = TextMessage(
    receiverUid: Constants.groupID,
    text: message,
    messageType: .text,
    receiverType: .group)

}

TextMessage is a CometChat class that contains information about, you guessed it, a text message. Here you create a new text message. Once created, you can send it. Add the following code to the method:

CometChat.sendTextMessage(
  message: textMessage,
  onSuccess: { [weak self] _ in
    guard let self = self else { return }
    print("Message sent")
    DispatchQueue.main.async {
      self.onRecievedMessage?(Message(
        user: user, 
        content: message, 
        isIncoming: false))
    }
  },
  onError: { error in
    print("Error sending message:")
    print(error?.errorDescription ?? "")
})

Call CometChat’s function for sending messages and, if the result is successful, call the message received callback so that the message is displayed in the UI. If something goes wrong, print out the error.

Head back to ChatViewController.swift, and add the following line to the bottom of sendMessage to call the method you just created:

ChatService.shared.send(message: message)

Now, finally, you can build the project and actually send and receive messages!

You’re actually sending and receiving chat messages!

You can run two simulator instances at once and chat with yourself. Chatting with yourself — a fun thing to do if you’re bored on a Sunday afternoon.

If you run the app multiple times, you might notice a small issue. Currently, all your messages get reset when you restart the app. Thankfully, CometChat lets you easily load previous messages.

Loading existing messages

Head back to ChatService.swift and add the following property:

private var messagesRequest: MessagesRequest?

This request object contains information about how to load messages from CometChat. Let’s continue by adding a new method to the class to load messages:

func getMessages(onComplete: @escaping ([Message])-> Void) {
  guard let user = user else {
    return
  }
  
  let limit = 50
  
  messagesRequest = MessagesRequest.MessageRequestBuilder()
    .set(limit: limit)
    .set(guid: Constants.groupID)
    .build()
}

Here we use MessageRequestBuilder to build out a request for the last 50 messages that were sent to the group. Once we have the request, it’s time to execute it. Add the following to the method:

messagesRequest!.fetchPrevious(
  onSuccess: { fetchedMessages in
    print("Fetched \(fetchedMessages?.count ?? 0) older messages")
    guard let fetchedMessages = fetchedMessages else {
      onComplete([])
      return
    }
    
    let messages = fetchedMessages
      .compactMap { $0 as? TextMessage }
      .map { Message($0, isIncoming: $0.senderUid != user.name.lowercased()) }
    
    DispatchQueue.main.async {
      onComplete(messages)
    }
},
  onError: { error in
    print("Fetching messages failed with error:")
    print(error?.errorDescription ?? "unknown")
})

Execute the request by calling fetchPrevious . If successful, first, check that messages were loaded, otherwise complete with an empty array. Then, filter the messages to only TextMessages (since we only support text messages in our app), and then convert them to Message, the model struct you use in your view controller. If the sender’s ID matches the currently logged in user’s ID, that’s an outgoing message. Otherwise, it’s incoming.

Finally, call onComplete on the main thread with the messages. Otherwise, if you get an error, simply print it out.

Let’s call this method from ChatViewController.swift. Add the following to viewWillAppear:

ChatService.shared.getMessages { [weak self] messages in
  self?.messages = messages
  self?.scrollToLastCell()
}

This code is pretty self-explanatory: get the messages, set them to the array and scroll to the last cell.

Run the project now and you should see your old messages!

You just built an iOS chat app!

Give yourself a pat on the back — you just built a working iOS chat app!

Conclusion

In this iOS chat tutorial, you saw how to create a working chat app with the help of CometChat. You also saw how to manage the keyboard and display new chat messages in a table view. I’d call that a good day’s work!

There are a lot of ways you can take this app even further. Besides text, CometChat supports sending images and even your own custom message types, perfect for sending attachments and files. You can also improve your users’ experience by adding typing indicators and indicating if a message was read. If you’re not a fan of chatting, CometChat also supports calling! With a flexible tool, the possibilities are endless.