How To Build A Drop-in Video Chat Application in iOS
Tags: ios, swift, agoraio
Published: 2020-07-09 20:12:31

Many cities and states have been under lockdown since the outbreak of the coronavirus epidemic. During this difficult time, we’re all looking for new ways to stay connected and support each other. This is when social networking applications such as [Houseparty](https://apps.apple.com/us/app/houseparty/id1065781769) become especially relevant and helpful.

These applications let users meet up and have fun with their friends without having to leave their homes. Users can enter their friend’s virtual room by just clicking a button. Houseparty, in particular, also provides some built-in games that users can play together.

If you’ve ever wondered how these cool applications are made, read on! This blog post will help get you started on the basics of building a similar social networking application in iOS.

### Prerequisites

1. A basic understanding of Swift and the iOS SDK.
2. An [Agora.io developer account](https://www.agora.io/en/blog/how-to-get-started-with-agora?utm_source=medium&utm_medium=blog&utm_campaign=de_content).
3. Xcode and an iOS device.
4. CocoaPods (If you don’t have CocoaPods installed already, you can find instructions [here](https://guides.cocoapods.org/using/getting-started.html)).

#### Overview

This guide will go over the steps for building a social networking application similar to [Houseparty](https://apps.apple.com/us/app/houseparty/id1065781769). This is a list of the core features that will be included in our app:

- Users can create and login into their account. User account information will be saved in Google Firebase Realtime Database.
- Users can set up virtual rooms to host video calls.
- Users can configure the accessibility of their virtual rooms. “Public” rooms are open for all friends to join and “private” rooms are invitation-only.
- During a video call, users can send private messages to another user in the same room by double-clicking on that user’s remote video.
- Users can chat with friends who are not in the room by clicking a button next to their names in the friend list.

> You can find my [Github demo app](https://bit.ly/3gynPus) as a reference for this article.

### Setting Up a New Project

To start, let's open up Xcode and create a new, blank project.

1. Open Xcode and select **New Project**.
2. Select **Single View App**.
3. Name and finalize your project. Make sure the language is set as **Swift**.

#### Setting up CocoaPods

1. In Terminal, navigate to the root directory of your project and run `pod init` to initialize CocoaPods.
2. Open the Podfile that was created and add the pods for the Agora library, as well as the Firebase libraries we'll use for user management:

```
target 'Your App' do
  use_frameworks!

  pod 'AgoraRtcEngine_iOS'
  pod 'AgoraRtm_iOS'
  pod 'Firebase/Analytics'
  pod 'Firebase/Auth'
  pod 'Firebase/Database'
  pod 'FirebaseUI'
end
```

3. Run `pod install` in Terminal to install the libraries.
4. From now on, open **YourApp.xcworkspace** to edit and run your app.

#### Setting up Firebase

Go to https://console.firebase.google.com and create a new Firebase project. Follow the instructions there to set up Firebase within your existing app. We're going to be using Firebase for authentication, analytics, and user management.

Once you've finished going through Firebase's setup, you should have completed the following steps:

1. Register your app's Bundle ID with Firebase. (As a reminder, you can find your Bundle ID in your project settings, under General)
2. Download the `GoogleService-Info.plist` file and add it to your app.
3. Import Firebase to your AppDelegate, and call `FirebaseApp.configure()` in `didFinishLaunchingWithOptions`.
4. Run your application and have Firebase verify communication.

You will then be presented with the Firebase dashboard. Go to the Develop pane, where you'll find the Authentication section.

Click on the "Set up sign-in method" button to move to the sign-in method pane. Enable the Email/Password and Google sign-in options. You'll need to set your public-facing app name and support email to do so.

In Xcode, you'll need to set up a URL scheme to handle Google sign-in. Copy the `REVERSED_CLIENT_ID` field from your `GoogleService-Info.plist`, and open up the URL Types pane in the Info section of your project settings:

![URL Schemes](https://miro.medium.com/max/1400/1*q7JkjCxm3jNRV4xWYEVPNQ.png)

Add a new URL type and paste the reversed client ID into the URL Schemes field. We'll also need to write some code so our app knows how to handle that URL. We'll be using Firebase UI, so for us it's as simple as just telling Firebase to handle it. Add the following to your `AppDelegate.swift`:

```
import FirebaseUI

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as! String?
    if FUIAuth.defaultAuthUI()?.handleOpen(url, sourceApplication: sourceApplication) ?? false {
        return true
    }

    return false
}
```

>There are plenty of other sign-in options that you may want to allow, but we won't be covering them here.

#### Setting up the View

![Storyboard Layout](https://miro.medium.com/max/1400/1*Phup8scjoRQitSQqRJSx4Q.png)

In your Xcode project, update the `Main.storyboard` and add a NavigationController. We're going to be using a `UICollectionView` to manage our video streams, so update your root view controller by adding a `UICollectionView` with a custom `UICollectionViewCell`. We're keeping it simple, so all you need in this custom class is a view to show video:

```
class VideoCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var videoView: UIView!    
}
```

We'll also need a bunch of buttons to handle all the user's actions:

```
@IBAction func didToggleMute(_ sender: Any) {

}

@IBAction func didTapSwitchCamera(_ sender: Any) {

}

@IBAction func didTapX(_ sender: Any) {

}

@IBAction func didTapInvite(_ sender: Any) {

}

@IBAction func didTapAddFriend(_ sender: Any) {

}
```

#### Logging in with FirebaseUI

>In this tutorial, we'll be using Firebase's built-in UI to handle sign-in for us. If you already have a login page, or simply want to be more flexible with your UI, you can find the docs for logging in programmatically with email and Google [here](https://firebase.google.com/docs/auth/ios/password-auth?authuser=0) and [here](https://firebase.google.com/docs/auth/ios/google-signin?authuser=0), respectively.

We're going to be using FirebaseUI to log in to our app. We'll have our initial entry screen - our `AgoraVideoViewController` - handle showing the default FUIAuth View Controller. All we need to do is tell it what providers we want to allow, and who to tell when the user successfully logs in:

```
import FirebaseAuth
import FirebaseUI

extension AgoraVideoViewController: FUIAuthDelegate {
    func showFUIAuthScreen() {
        let authUI = FUIAuth.defaultAuthUI()
        authUI?.delegate = self

        let providers: [FUIAuthProvider] = [
            FUIGoogleAuth(),
            FUIEmailAuth()
        ]
        authUI?.providers = providers

        if let authViewController = authUI?.authViewController() {
            self.present(authViewController, animated: false)
        }
    }

    func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {

    }
}

```

We could call this function on startup, but it would get pretty annoying to have to log in every time we open the app. To solve this, we can use something provided to us by FirebaseAuth - an `AuthStateDidChangeListener`. It will tell us whenever the user's authentication state changes, and allow us to only show the login page if there's no user already logged in. Adding one is pretty simple:

```
var handle: AuthStateDidChangeListenerHandle?
var currentUser: User?

override func viewWillAppear(_ animated: Bool) {
    handle = Auth.auth().addStateDidChangeListener { (auth, user) in
        self.currentUser = user
        if user == nil {
            self.showFUIAuthScreen()
        }
    }
}

override func viewWillDisappear(_ animated: Bool) {
    if let handle = handle {
        Auth.auth().removeStateDidChangeListener(handle)
    }
}
```

We now have a functional login page that will appear if the current user is nil.

#### Creating a User Database

Firebase will track our users for us - you can see this for yourself on the Authentication tab of the Firebase dashboard, once we implement signing in. However, this list of users isn't very useful to us. While we can get information from it about the currently logged-in user, it won't allow us to get any info about other users. We'll need our own database for that.

Go to the Database tab on the Firebase dashboard, and create a new Realtime Database. Start it in test mode for now, so we can easily modify it without having to worry about security while we're working on it. We could add data manually here, but it'll be easier to do it automatically in code.

![Creating a Realtime Database](https://miro.medium.com/max/1400/1*PHaHVPWSmXLR7YmvgpwgXQ.png)

#### Adding users on login

Head back to our `FUIAuthDelegate` extension. We're going to make use of that `didSignInWith` callback to add a user to our database whenever they log in:

```
func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
    if let error = error {
        print(error.localizedDescription)
    } else {

        //Save the user to our list of users.
        if let user = authDataResult?.user {
            let ref = Database.database().reference()
            ref.child("users").child(user.uid).setValue(["username" : user.displayName?.lowercased(),
                                                         "displayname" : user.displayName,
                                                         "email": user.email])
        }
    }
}
```

This code gets a reference to our main database, and adds an entry in a new "users" node. Each child of a node needs to have a unique key, so we use the unique UID Firebase gives us, and we store the user's email, their display name, and a lowercased version of their display name that will make it easier to search for it later.

>Note that this code will overwrite our user node every time the user logs in. If you want to add additional fields to our user database, this code will need to be adjusted so it doesn't delete things.

### Joining a Video Call

Once the user has logged in, they should automatically be placed into their own personal room so that their friends can drop by. To do that, we'll be using the Agora SDK to quickly and easily handle video calls for us.

#### Add Camera and Microphone Permissions

In order to use the microphone and camera, we’ll need to ask the user for permission to do so. In your Info.plist add the following keys:

```
Privacy - Microphone Usage Description
Privacy - Camera Usage Description
```

Make sure you add a value for each. These values are user-facing, and will be displayed when the app asks for these permissions from the user.

#### Initialize the Agora Engine

In order to do anything with Agora, we need an `AgoraRtcEngineKit` object initialized with our appID (acquired from the [Agora Developer Console](https://console.agora.io)). Let's add a helper function that will give us this object when we need it, creating it first if necessary.

```
import AgoraRtcEngineKit

let appID = "YourAppIDHere"
var agoraKit: AgoraRtcEngineKit?
let tempToken: String? = nil //If you have a token, put it here.
var userID: UInt = 0 //This tells Agora to generate an id for you. If you have unique user IDs already, you can use those.

...

private func getAgoraEngine() -> AgoraRtcEngineKit {
    if agoraKit == nil {
        agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: appID, delegate: self)
    }
    return agoraKit!
}
```

#### Enable Video

Next, we need to tell Agora we want to enable the video, and set up the video configuration.

```
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.    
    getAgoraEngine().setChannelProfile(.communication)

    setUpVideo()
}

func setUpVideo() {
    getAgoraEngine().enableVideo()
    let configuration = AgoraVideoEncoderConfiguration(size:
                        AgoraVideoDimension640x360, frameRate: .fps15, bitrate: 400,
                        orientationMode: .fixedPortrait)
    getAgoraEngine().setVideoEncoderConfiguration(configuration)
}
```

#### Join a Call

We want users to join a call as soon as they log in. To make sure each one is placed into their own personal channel, we'll use their uid as the channel name.

Update your `viewWillAppear` and add a new function for joining a call:

```
var inCall = false
var callID: UInt = 0
var channelName: String?

override func viewWillAppear(_ animated: Bool) {
    handle = Auth.auth().addStateDidChangeListener { (auth, user) in
        self.currentUser = user
        if user == nil {
            self.showFUIAuthScreen()
        } else {
            self.joinChannel(channelName: user!.uid)
        }
    }
}

func joinChannel(channelName: String) {
    getAgoraEngine().joinChannel(byToken: tempToken, channelId: channelName, info: nil, uid: callID) { [weak self] (sid, uid, elapsed) in
        self?.inCall = true
        self?.callID = uid
        self?.channelName = channelName
    }
}
```

> Note: Agora uses UInt IDs to identify individual users within a call. Our Firebase UIDs won't work for this, because they're strings, and it's not important that they stay the same from call to call, so we just pass in 0 and save whatever Agora assigns us for later use.

#### Displaying Video

It's finally time to put our `UICollectionView` to good use. First, we're going to implement some Agora delegate functions so we can detect when other users join our call, and keep track of their in-call ids.

```
var remoteUserIDs: [UInt] = []

...

extension AgoraVideoViewController: AgoraRtcEngineDelegate {
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
        callID = uid
    }

    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
        print("Joined call of uid: \(uid)")
        remoteUserIDs.append(uid)
        collectionView.reloadData()
    }

    func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
        if let index = remoteUserIDs.firstIndex(where: { $0 == uid }) {
            remoteUserIDs.remove(at: index)
            collectionView.reloadData()
        }
    }
}
```

And then we need to set up our collection view cells to display video:

```
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return remoteUserIDs.count + 1
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath)

    if indexPath.row == remoteUserIDs.count { //Put our local video last
        if let videoCell = cell as? VideoCollectionViewCell {
            let videoCanvas = AgoraRtcVideoCanvas()
            videoCanvas.uid = callID
            videoCanvas.view = videoCell.videoView
            videoCanvas.renderMode = .fit
            getAgoraEngine().setupLocalVideo(videoCanvas)
        }
    } else {
        let remoteID = remoteUserIDs[indexPath.row]
        if let videoCell = cell as? VideoCollectionViewCell {
            let videoCanvas = AgoraRtcVideoCanvas()
            videoCanvas.uid = remoteID
            videoCanvas.view = videoCell.videoView
            videoCanvas.renderMode = .fit
            getAgoraEngine().setupRemoteVideo(videoCanvas)
            print("Creating remote view of uid: \(remoteID)")
        }
    }

    return cell
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    let numFeeds = remoteUserIDs.count + 1

    let totalWidth = collectionView.frame.width - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right
    let totalHeight = collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom

    if numFeeds == 1 {
        return CGSize(width: totalWidth, height: totalHeight)
    } else if numFeeds == 2 {
        return CGSize(width: totalWidth, height: totalHeight / 2)
    } else {
        if indexPath.row == numFeeds {
            return CGSize(width: totalWidth, height: totalHeight / 2)
        } else {
            return CGSize(width: totalWidth / CGFloat(numFeeds - 1), height: totalHeight / 2)
        }
    }
}
```

> Note: Make sure you set your view controller as a UICollectionViewDelegateFlowLayout. Learn from my mistakes.

### Working With Friends

It's time to start keeping track of a user's friends. We're going to add two popover views to our app - one for searching for users to add as friends, and one for displaying a friends list that we can use to join other user's rooms.

#### Searching for Users

We're going to create a `UserSearchViewController` to search through our user database for possible new friends.

![UserSearchViewController](https://miro.medium.com/max/1400/1*Vx7nMXaYw0gD08j83RLwOg.png)

We need a UISearchBar, and a UITableView to display the results. We'll make a very simple UserTableViewCell class for our cells:

```
class UserTableViewCell: UITableViewCell {

    @IBOutlet weak var displayName: UILabel!
    @IBOutlet weak var detailLabel: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

    override func prepareForReuse() {
        detailLabel.alpha = 0
    }

}
```

Make sure to hook everything up to our UserSearchViewController class, and make sure to set the ViewController as the initial view controller. Then let's show our new View Controller when our user hits the "Add Friends" button. In our main `AgoraVideoViewController` class:

```
@IBAction func didTapAddFriend(_ sender: Any) {
    let storyboard = UIStoryboard(name: "UserSearchViewController", bundle: nil)
    let searchVC = storyboard.instantiateInitialViewController()!

    // Use the popover presentation style for your view controller.
    searchVC.modalPresentationStyle = .popover

    // Present the view controller (in a popover).
    self.present(searchVC, animated: true) {

    }
}
```

We set up some basic stuff when we load:

```
import UIKit
import Firebase

class UserSearchViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {

    @IBOutlet weak var tableView: UITableView!

    var userRef: DatabaseReference!
    var friendsRef: DatabaseReference!
    var resultsArray = [[String:String]]()

    var handle: AuthStateDidChangeListenerHandle?
    var currentUser: User?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        userRef = Database.database().reference(withPath: "users")
        friendsRef = Database.database().reference(withPath: "friends")
    }

    override func viewWillAppear(_ animated: Bool) {
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            self.currentUser = user
            if user == nil {
                DispatchQueue.main.async {
                    self.dismiss(animated: true, completion: nil)
                }
            }
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        if let handle = handle {
            Auth.auth().removeStateDidChangeListener(handle)
        }
    }
```

Most of our complex logic comes when the user actually searches:

```
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    resultsArray.removeAll()
    tableView.reloadData()
}

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    if let searchText = searchBar.text?.lowercased(), searchText != "" {
        resultsArray.removeAll()
        queryText(searchText, inField: "username")
    } else {
        let alert = UIAlertController(title: "Error", message: "Please enter a username.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
}

func queryText(_ text: String, inField child: String) {
    userRef.queryOrdered(byChild: child)
        .queryStarting(atValue: text)
        .queryEnding(atValue: text+"\u{f8ff}")
        .observeSingleEvent(of: .value) { [weak self] (snapshot) in
            for case let item as DataSnapshot in snapshot.children {
                //Don't show the current user in search results
                if self?.currentUser?.uid == item.key {
                    continue
                }

                if var itemData = item.value as? [String:String] {
                    itemData["uid"] = item.key
                    self?.resultsArray.append(itemData)
                }
            }
            self?.tableView.reloadData()
    }
}

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

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

    if let userCell = cell as? UserTableViewCell {
        let userData = resultsArray[indexPath.row]
        userCell.displayName.text = userData["displayname"]
    }

    return cell
}
```

When the user hits the search button, we perform a database query to return all users whose username starts with the text entered.

> Tip: If you don't have multiple phones to test with, you can always add dummy users into your database directly in the Firebase console.

If you run your app and search for another user, they will now appear in your list! Very cool. However, you may also notice Firebase complaining at you in the console:

_[Firebase/Database][I-RDB034028] Using an unspecified index. Your data will be downloaded and filtered on the client. Consider adding ".indexOn": "username" at /users to your security rules for better performance_

This is Firebase telling us that it's not indexing our users by our search fields, because we haven't told it to. With as few users as we have now, it's not a big deal, but if we want to release to a large userbase, we should fix this. Fortunately, adding the rule is easy. Head to the Database tab in your Firebase dashboard, and open up your Rules. Add the `.indexOn` field to your users database and hit Publish:

![Indexing on Username](https://miro.medium.com/max/1400/1*aljlIndKpB_GtA2ca2Gyxg.png)

#### Adding a Friend

Finally, we need to add a user to our friends list when the user selects them. To do that, we're going to create a "friends" database alongside our user database and add a node to it:

```
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if let user = currentUser, let friendID = resultsArray[indexPath.row]["uid"] {
        friendsRef.child("\(user.uid)/\(friendID)").setValue("true")
        if let userCell = tableView.cellForRow(at: indexPath) as? UserTableViewCell {
            UIView.animate(withDuration: 0.2) {
                userCell.detailLabel.alpha = 1
            }
        }
    }

    tableView.deselectRow(at: indexPath, animated: true)
}
```

#### Viewing Your Friends

Now that we have friends on our friends list, it's time to use that to join other people's rooms. Let's make one more view controller.

![Join Friend](https://miro.medium.com/max/1400/1*_6zN3WYCEo0WeM1Y0IjAJA.png)

We need a UITableView and that's about it. We can even re-use our UserTableViewCell class. Again, we show it in a popover when the user taps a button. This time, though, we're going to create a protocol for our new view controller to tell our `AgoraVideoViewController` which friend's room we want to join.

```
@IBAction func didTapInvite(_ sender: Any) {
    let storyboard = UIStoryboard(name: "JoinFriendViewController", bundle: nil)
    let joinVC = storyboard.instantiateInitialViewController()!

    // Use the popover presentation style for your view controller.
    joinVC.modalPresentationStyle = .popover

    if let joinFriendVC = joinVC as? JoinFriendViewController {
        joinFriendVC.delegate = self
    }

    // Present the view controller (in a popover).
    self.present(joinVC, animated: true) {

    }
}
```

And in our new class:

```
import UIKit
import Firebase

protocol JoinFriendViewControllerDelegate: NSObject {
    func didJoinFriend(uid: String)
}

class JoinFriendViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!

    var userRef: DatabaseReference!
    var friendsRef: DatabaseReference!
    var resultsArray = [String]()

    var handle: AuthStateDidChangeListenerHandle?
    var currentUser: User?

    weak var delegate: JoinFriendViewControllerDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        userRef = Database.database().reference(withPath: "users")
        friendsRef = Database.database().reference(withPath: "friends")
    }
```

When the view appears, we'll get our usual reference to the current user, and use that to get each friend's ID:

```
override func viewWillAppear(_ animated: Bool) {
    handle = Auth.auth().addStateDidChangeListener { (auth, user) in
        self.currentUser = user
        self.resultsArray.removeAll()
        self.tableView.reloadData()
        if let user = user {
            //Create an observer that will let us know when friends are added.
            self.friendsRef.child(user.uid).observe(.childAdded) { (snapshot) in
                self.resultsArray.append(snapshot.key)
                self.tableView.insertRows(at: [IndexPath(row: self.resultsArray.count-1, section: 0)], with: UITableView.RowAnimation.none)

            }
        } else {
            DispatchQueue.main.async {
                self.dismiss(animated: true, completion: nil)
            }
        }
    }
}

override func viewWillDisappear(_ animated: Bool) {
    if let handle = handle {
        Auth.auth().removeStateDidChangeListener(handle)
    }
}
```

And then we display each friend's username, and pass the ID back to the video view when selected:

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

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

    if let userCell = cell as? UserTableViewCell {
        let uid = resultsArray[indexPath.row]
        userRef.child(uid).child("displayname").observeSingleEvent(of: .value) { (snapshot) in
            userCell.displayName.text = snapshot.value as? String
        }
    }

    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let uid = resultsArray[indexPath.row]

    delegate?.didJoinFriend(uid: uid)

    self.dismiss(animated: true, completion: nil)
}
```

Finally, back in our `AgoraVideoViewController` we handle the message and change rooms accordingly.

```
func didJoinFriend(uid: String) {
    joinFriendCallWithUID(uid: uid)
}

func joinFriendCallWithUID(uid: String) {
    leaveChannel()
    joinChannel(channelName: uid)
}

func leaveChannel() {
    getAgoraEngine().leaveChannel(nil)
    inCall = false
    remoteUserIDs.removeAll()
    collectionView.reloadData()
}
```

### Locking Rooms

Sometimes, a user wants to set his own virtual room to become a “private” room so that no one else can join his room except those who are already in the room. So how should we achieve that?

First, we'll need to know whether the current room belongs to us. Then, we'll allow the host to lock the room with a button, and save that state to the database.

```
var isLocalCall = true {
    didSet {
        updateLockTitle()
    }
}
var callLocked = false {
    didSet {
        updateLockTitle()
    }
}

func updateLockTitle() {
    if isLocalCall {
        if callLocked {
            lockButton.setTitle("Unlock", for: .normal)
        } else {
            lockButton.setTitle("Lock", for: .normal)
        }
    } else {
        lockButton.setTitle("Exit", for: .normal)
    }
}
```

Add the following line to the success callback of `joinChannel`:

```
self?.isLocalCall = channelName == self?.currentUser?.uid
```

And then implement the handler for the button:

```
@IBAction func didTapX(_ sender: Any) {
    if inCall {
        if (isLocalCall) {
            //Toggle lock on the room
            callLocked = !callLocked
            if (callLocked) {
                userRef.child("\(currentUser!.uid)/locked").setValue("true")
            } else {
                userRef.child("\(currentUser!.uid)/locked").setValue("false")
            }
        } else {
            leaveChannel()
            if let user = currentUser {
                joinChannel(channelName: user.uid)
            }
        }
    }
}
```

Our channels can now be locked and that state is saved. But it doesn't do anything yet. We need to check whether a room is locked when we try to join it. In our `JoinFriendViewController` let's first show the user if a room is locked by adding the following to `cellForRowAt:`

```
userRef.child(uid).child("locked").observe(.value) { (snapshot) in
    if let lockState = snapshot.value as? String, lockState == "true" {
        userCell.detailLabel.alpha = 1
    } else {
        userCell.detailLabel.alpha = 0
    }
}
```

Unlike with `observeSingleEvent`, the `observe` function will be called whenever the value being observed changes. Which is great for us, because it means the cell will automatically update as soon as any of our friends locks or unlocks their room.

We can then update our `didSelectRowAt` function to show an alert if the user tries to join a locked room:

```
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let uid = resultsArray[indexPath.row]

  userRef.child(uid).child("locked").observeSingleEvent(of: .value) { (snapshot) in
      if let lockState = snapshot.value as? String, lockState == "true" {
          DispatchQueue.main.async {
              self.tableView.deselectRow(at: indexPath, animated: true)
              let alert = UIAlertController(title: "Locked", message: "That user's room is currently locked.", preferredStyle: .alert)
              alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
              self.present(alert, animated: true, completion: nil)
          }
      } else {
          self.delegate?.didJoinFriend(uid: uid)

          self.dismiss(animated: true, completion: nil)
      }
  }
}
```

And that's all there is to it!

### Adding Text Chat

We’re going to use Agora’s Real-Time Messaging (RTM) SDK in order to allow users to chat with each other while they’re in a video call. First, let’s set up the AgoraRtmKit.

```
var agoraRtm: AgoraRtmKit?

override func viewDidLoad() {
    ...
        
    agoraRtm = AgoraRtmKit.init(appId: appID, delegate: self)
}

override func viewWillAppear(_ animated: Bool) {
    handle = Auth.auth().addStateDidChangeListener { (auth, user) in
        self.currentUser = user
        if let user = user {
            self.joinChannel(channelName: user.uid)
            self.agoraRtm?.login(byToken: nil, user: user.displayName ?? user.uid) { (error) in
                if (error != .ok) {
                    print("Failed to login to RTM: ", error.rawValue)
                }
            }
        } else {
            self.agoraRtm?.logout(completion: nil)
            self.showFUIAuthScreen()
        }
    }
}

...

extension AgoraVideoViewController: AgoraRtmDelegate {
    func rtmKit(_ kit: AgoraRtmKit, connectionStateChanged state: AgoraRtmConnectionState, reason: AgoraRtmConnectionChangeReason) {
        if state == .connected {
            chatButton.isEnabled = true
        } else {
            chatButton.isEnabled = false
        }
    }
}
```

We use the same Agora app ID we already have, and then we login and logout of RTM when our user does. We also set up the AgoraRtmDelegate protocol. We’re only going to use it to make sure our chat button isn’t clickable until we’ve finished logging in.

For the chat room itself, we’ll need one final view controller.

![Chat View Controller](https://miro.medium.com/max/1400/1*7uvEQR2DHExLi2QpY9IMGg.png)

We need a UITextField to send messages, and a table view to display them. We again have a very simple cell:

```
class ChatTableViewCell: UITableViewCell {
    
    @IBOutlet weak var messageLabel: UILabel!
    
}
```

Like before, we’re going to present this view as a popover when the user hits the ‘Chat’ button.

#### Joining a Text Channel

In our ChatViewController, we’re going to do some very familiar setup to make sure we have a user at all times, and then join an RTM channel in viewWillAppear:

```
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var textField: UITextField!
    
    weak var agoraRtm: AgoraRtmKit?
    var channelName: String?
    var channel: AgoraRtmChannel?
    
    var handle: AuthStateDidChangeListenerHandle?
    var currentUser: User?
    
    var messageList: [String] = []
    
    override func viewWillAppear(_ animated: Bool) {
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            self.currentUser = user
            if user != nil, let channelName = self.channelName {
                self.channel = self.agoraRtm?.createChannel(withId: channelName, delegate: self)
                self.channel?.join(completion: { (error) in
                    if error != .channelErrorOk {
                        print("Error joining channel: ", error.rawValue)
                    }
                })
            } else {
                DispatchQueue.main.async {
                    self.dismiss(animated: true, completion: nil)
                }
            }
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        if let handle = handle {
            Auth.auth().removeStateDidChangeListener(handle)
        }
        if let channel = self.channel {
            channel.leave(completion: nil)
        }
    }
}
```

The createChannel method will create a channel if one doesn’t exist, or join an existing one if it does. Like with Agora video channels, everyone that joins a channel with the same name will be able to chat with each other.

#### Showing Messages

In order to display a conversation, we need to do two things: Send messages when the local user types them, and receive messages from everyone else.

```
var messageList: [String] = []

func addMessage(user: String, message: String) {
    let message = "\(user): \(message)"
    messageList.append(message)
    let indexPath = IndexPath(row: self.messageList.count-1, section: 0)
    self.tableView.insertRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
    self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    if let text = textField.text, text != "" {
        channel?.send(AgoraRtmMessage(text: text), completion: { (error) in
            if error != .errorOk {
                print("Failed to send message: ", error)
            } else {
                self.addMessage(user: self.currentUser!.displayName ?? self.currentUser!.uid, message: text)
            }
        })
        textField.text = ""
    }
    return true
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    messageList.count
}
    
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "chatCell", for: indexPath)

    if let chatCell = cell as? ChatTableViewCell {
        let message = messageList[indexPath.row]
        chatCell.messageLabel.text = message
    }

    return cell
}

...

extension ChatViewController: AgoraRtmChannelDelegate {
    func channel(_ channel: AgoraRtmChannel, messageReceived message: AgoraRtmMessage, from member: AgoraRtmMember) {
        addMessage(user: member.userId, message: message.text)
    }
}
```

Whenever the user sends or receives a message, we add it to our message data, insert a new table row, and then scroll to the bottom to make sure we see it.

#### Handling the Keyboard

If you try to test the app now, you’ll notice an immediate problem: Our text field is at the bottom of the screen, and the keyboard covers it up when you select it. Let’s fix that.

```
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
    NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
    textField.becomeFirstResponder()
}

@objc func keyboardWillShow(notification: NSNotification) {
    guard let userInfo = notification.userInfo else { return }
    guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }

    let keyboardFrame = keyboardSize.cgRectValue

    bottomConstraint.constant = 20 + keyboardFrame.height
}

@objc func keyboardWillHide(notification: NSNotification) {
    bottomConstraint.constant = 20
}
```

Here, we add a reference to the NSLayoutConstraint attaching the text field to the bottom of the screen. Using Notification Center, we can then find out when the keyboard is shown or hidden, and adjust how far from the bottom of the screen our text field is automatically.

#### Finishing Touches

Finally, let's add the functionality to our final two buttons, both of which are a simple call to an Agora API. To switch the camera:

```
@IBAction func didTapSwitchCamera(_ sender: Any) {
    getAgoraEngine().switchCamera()
}
```

And to mute our local audio:

```
var muted = false {
    didSet {
        if muted {
            muteButton.setTitle("Unmute", for: .normal)
        } else {
            muteButton.setTitle("Mute", for: .normal)
        }
    }
}

@IBAction func didToggleMute(_ sender: Any) {
    muted = !muted
    getAgoraEngine().muteLocalAudioStream(muted)
}
```

#### Congratulations

If you've made it this far, congratulations! You have a working social app. The best way to test it is to build it and run on multiple phones, but you can also use Agora's [web demo](https://webdemo.agora.io/agora-web-showcase/) to substitute as additional users.

Thank you for following along. If you want to see more features such as push notifications or presence tracking, please leave a comment below! If you have any questions, you can also reach out via email at devrel@agora.io.