<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: zontan</title>
    <description>The latest articles on DEV Community by zontan (@zontan).</description>
    <link>https://dev.to/zontan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F416855%2Fe2faf2be-ef29-4e38-8914-8dd1719171ca.png</url>
      <title>DEV Community: zontan</title>
      <link>https://dev.to/zontan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zontan"/>
    <language>en</language>
    <item>
      <title>Building a 1-to-many iOS video app with Agora</title>
      <dc:creator>zontan</dc:creator>
      <pubDate>Thu, 16 Jul 2020 17:52:12 +0000</pubDate>
      <link>https://dev.to/zontan/building-a-1-to-many-ios-video-app-with-agora-1h5n</link>
      <guid>https://dev.to/zontan/building-a-1-to-many-ios-video-app-with-agora-1h5n</guid>
      <description>&lt;p&gt;Who wants to build a video chat app in a hour? This is a guide to how to quickly and easily create a video chat app that can support multiple participants with the Agora SDK.&lt;/p&gt;

&lt;h4&gt;
  
  
  Requirements
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt; Xcode 10.0+&lt;/li&gt;
&lt;li&gt;A physical iOS device. The iOS simulator lacks camera functionality.&lt;/li&gt;
&lt;li&gt;Cocoapods (If you don't have Cocoapods installed already, you can find instructions &lt;a href="https://guides.cocoapods.org/using/getting-started.html"&gt;here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;An Agora account (You can sign up for free &lt;a href="https://sso.agora.io/en/signup"&gt;here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;An understanding of how to build iOS layouts with a Storyboard. If you need a refresher, there's a great tutorial &lt;a href="https://www.raywenderlich.com/464-storyboards-tutorial-for-ios-part-1"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Setting up the Agora Library with Cocoapods
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In Terminal, navigate to the root directory of your project and run &lt;code&gt;pod init&lt;/code&gt; to initialize Cocoapods.&lt;/li&gt;
&lt;li&gt;Open the Podfile that was created and add the following code to import the Agora library:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target 'Your App' do
  pod 'AgoraRtcEngine_iOS'
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pod install&lt;/code&gt; in Terminal to install the library.&lt;/li&gt;
&lt;li&gt;From now on, open &lt;strong&gt;YourApp.xcworkspace&lt;/strong&gt; to edit and run your app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Add Camera and Microphone permissions
&lt;/h4&gt;

&lt;p&gt;In order to use the microphone and camera, we'll need to ask the user for permission to do so. In your &lt;code&gt;Info.plist&lt;/code&gt; add the following keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Privacy - Microphone Usage Description
Privacy - Camera Usage Description
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting up the scene
&lt;/h4&gt;

&lt;p&gt;In our &lt;code&gt;Main.storyboard&lt;/code&gt; we'll need to add the views Agora will use to display the video feeds. For our demo, we'll be using a single large view to display our local feed, and a collection view to show an arbitrary number of remote users, but feel free to adjust as necessary for your own needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kHWBmx51--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1400/1%2AaNDOHlGDOVdlY0983lZ4Tg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kHWBmx51--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1400/1%2AaNDOHlGDOVdlY0983lZ4Tg.png" alt="Storyboard Layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The local view is in green, and the remote view template is in red, for ease of identification. Add a View object for the local stream, a UIButton to mute and hang up the call, and a UICollectionView to hold the remote streams. Your UICollectionViewCells can be as simple as a single view to hold the stream - in the example above, I've added an overlay to show the remote user's name if we know it.&lt;/p&gt;

&lt;p&gt;Make sure you hook up the views in your main View Controller, and set the View Controller as the UICollectionView's delegate and dataSource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AgoraVideoViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UICollectionViewDelegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UICollectionViewDataSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;collectionView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UICollectionView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;localVideoView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;muteButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIButton&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hangUpButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIButton&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And connect up your custom collection view cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;VideoCollectionViewCell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UICollectionViewCell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;videoView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;nameplateView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;usernameLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UILabel&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: If you want to add overlays to your video streams, make sure you don't add them as subviews of the view objects you're going to use as video screens. The video canvas will be drawn on top of them. Add them as sibling views instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Initialize the Agora Engine
&lt;/h4&gt;

&lt;p&gt;In order to use the Agora engine, we need to create an instance of &lt;code&gt;AgoraRtcEngineKit&lt;/code&gt; with our app ID.&lt;/p&gt;

&lt;p&gt;First, we will need to retrieve our app ID by going to the &lt;a href="https://dashboard.agora.io"&gt;Agora Dashboard&lt;/a&gt;. If you haven't created an Agora project yet, do so now by clicking "New Project."&lt;/p&gt;

&lt;p&gt;Once you have a project, click the "Edit" button (or open the Project Management pane) to view that project's details. Copy the app ID and add it to your project.&lt;br&gt;
If you enabled the App Certificate, you'll also need a Token to join channels - you can generate a temporary one by clicking "Generate Temp Token." You can also read our tutorial on generating your own tokens &lt;a href="https://docs.agora.io/en/Video/token_server?platform=CPP"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first call you make to Agora must be to initialize a shared Agora engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcKit&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;appID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YourAppIDHere"&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;agoraKit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineKit&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tempToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="c1"&gt;//If you have a token, put it here.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UInt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;//This tells Agora to generate an id for you. If you have unique user IDs already, you can use those.&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineKit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;agoraKit&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;agoraKit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sharedEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withAppId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agoraKit&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: This is a quick way to ensure the engine is only initialized once when you need it, but for a larger app you may want to consider wrapping it in a &lt;a href="https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton"&gt;Singleton&lt;/a&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We'll also need to implement the AgoraRtcEngineDelegate protocol so we can respond to relevant callbacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;AgoraVideoViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h4&gt;
  
  
  Enable Video
&lt;/h4&gt;

&lt;p&gt;The next step is to tell Agora we want video enabled, and to tell it where to put the local video stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;setUpVideo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableVideo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;videoCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcVideoCanvas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;
    &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localVideoView&lt;/span&gt;
    &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;
    &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setupLocalVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: If you want to customize how the video is displayed, this is a good place to &lt;a href="https://docs.agora.io/en/Video/videoProfile_ios?platform=iOS"&gt;configure the video profile&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Join a channel
&lt;/h4&gt;

&lt;p&gt;Once the engine is initialized, joining a call is as easy as calling joinChannel() on the Agora engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;joinChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;localVideoView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;byToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tempToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;    
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h4&gt;
  
  
  Setting up Remote Video
&lt;/h4&gt;

&lt;p&gt;Now is the time to put our UICollectionView to good use. We'll keep a list of remote user IDs, and for each one, set up a remote video canvas within our collection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UInt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;collectionView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;collectionView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UICollectionView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numberOfItemsInSection&lt;/span&gt; &lt;span class="nv"&gt;section&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;collectionView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;collectionView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UICollectionView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cellForItemAt&lt;/span&gt; &lt;span class="nv"&gt;indexPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IndexPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;UICollectionViewCell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collectionView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dequeueReusableCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withReuseIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"videoCell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;indexPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;remoteID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;indexPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;videoCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;VideoCollectionViewCell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;videoCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcVideoCanvas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remoteID&lt;/span&gt;
        &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;videoCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videoView&lt;/span&gt;
        &lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;
        &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setupRemoteVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoCanvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: Remember to set your custom cell's reuse identifier in your Main.Storyboard!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To get this list of userIDs (and maintain it), we'll utilize the &lt;code&gt;rtcEngine(didJoinedOfUid:)&lt;/code&gt; and &lt;code&gt;rtcEngine(didOfflineOfUid:)&lt;/code&gt; callbacks. Inside your &lt;code&gt;AgoraRtcEngineDelegate&lt;/code&gt; extension, add the following functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;rtcEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineKit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didJoinedOfUid&lt;/span&gt; &lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;collectionView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;rtcEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AgoraRtcEngineKit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didOfflineOfUid&lt;/span&gt; &lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AgoraUserOfflineReason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;collectionView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And with that, you have a working video chat app. Beware of feedback if testing on multiple devices at once.&lt;/p&gt;

&lt;h4&gt;
  
  
  Polish
&lt;/h4&gt;

&lt;p&gt;There a few more pieces that we should add in to make our app a little nicer. For one, our buttons don't do anything. Lets's fix that first. Enabling the mute button is a simple call to &lt;code&gt;adjustRecordingSignalVolume()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;didSet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;muted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;muteButton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unmute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;muteButton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;@IBAction&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;didToggleMute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;muted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adjustRecordingSignalVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adjustRecordingSignalVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;muted&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We can also hang up by calling &lt;code&gt;leaveChannel()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@IBAction&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;didTapHangUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;leaveChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;leaveChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leaveChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;localVideoView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;remoteUserIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;collectionView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: If you don't hide the local video view (or pop the view controller) you'll end up with a static view of the last frame recorded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a final touch, let's take advantage of Agora's ability to join channels with a username to give our remote streams some nice nameplates. We can update &lt;code&gt;joinChannel()&lt;/code&gt; to join with a username if we have one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;joinChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;localVideoView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;byUserAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tempToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;byToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tempToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And then we can extract that username when a remote user joins. Add the following block to &lt;code&gt;collectionView(cellForItemAt:)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;userInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAgoraEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;byUid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;remoteID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userAccount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;videoCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nameplateView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;videoCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usernameLabel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;videoCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nameplateView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And we're done! We have a lovely demo.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How To Build A Drop-in Video Chat Application in iOS</title>
      <dc:creator>zontan</dc:creator>
      <pubDate>Thu, 09 Jul 2020 20:12:31 +0000</pubDate>
      <link>https://dev.to/zontan/how-to-build-a-drop-in-video-chat-application-in-ios-1273</link>
      <guid>https://dev.to/zontan/how-to-build-a-drop-in-video-chat-application-in-ios-1273</guid>
      <description>&lt;p&gt;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 &lt;a href="https://apps.apple.com/us/app/houseparty/id1065781769" rel="noopener noreferrer"&gt;Houseparty&lt;/a&gt; become especially relevant and helpful.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A basic understanding of Swift and the iOS SDK.&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://www.agora.io/en/blog/how-to-get-started-with-agora?utm_source=medium&amp;amp;utm_medium=blog&amp;amp;utm_campaign=de_content" rel="noopener noreferrer"&gt;Agora.io developer account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Xcode and an iOS device.&lt;/li&gt;
&lt;li&gt;CocoaPods (If you don’t have CocoaPods installed already, you can find instructions &lt;a href="https://guides.cocoapods.org/using/getting-started.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Overview
&lt;/h4&gt;

&lt;p&gt;This guide will go over the steps for building a social networking application similar to &lt;a href="https://apps.apple.com/us/app/houseparty/id1065781769" rel="noopener noreferrer"&gt;Houseparty&lt;/a&gt;. This is a list of the core features that will be included in our app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can create and login into their account. User account information will be saved in Google Firebase Realtime Database.&lt;/li&gt;
&lt;li&gt;Users can set up virtual rooms to host video calls.&lt;/li&gt;
&lt;li&gt;Users can configure the accessibility of their virtual rooms. “Public” rooms are open for all friends to join and “private” rooms are invitation-only.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Users can chat with friends who are not in the room by clicking a button next to their names in the friend list.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;You can find my &lt;a href="https://bit.ly/3gynPus" rel="noopener noreferrer"&gt;Github demo app&lt;/a&gt; as a reference for this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Setting Up a New Project
&lt;/h3&gt;

&lt;p&gt;To start, let's open up Xcode and create a new, blank project.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Xcode and select &lt;strong&gt;New Project&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Single View App&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name and finalize your project. Make sure the language is set as &lt;strong&gt;Swift&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Setting up CocoaPods
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In Terminal, navigate to the root directory of your project and run &lt;code&gt;pod init&lt;/code&gt; to initialize CocoaPods.&lt;/li&gt;
&lt;li&gt;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:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target 'Your App' do
  use_frameworks!

  pod 'AgoraRtcEngine_iOS'
  pod 'AgoraRtm_iOS'
  pod 'Firebase/Analytics'
  pod 'Firebase/Auth'
  pod 'Firebase/Database'
  pod 'FirebaseUI'
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pod install&lt;/code&gt; in Terminal to install the libraries.&lt;/li&gt;
&lt;li&gt;From now on, open &lt;strong&gt;YourApp.xcworkspace&lt;/strong&gt; to edit and run your app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Setting up Firebase
&lt;/h4&gt;

&lt;p&gt;Go to &lt;a href="https://console.firebase.google.com" rel="noopener noreferrer"&gt;https://console.firebase.google.com&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;Once you've finished going through Firebase's setup, you should have completed the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register your app's Bundle ID with Firebase. (As a reminder, you can find your Bundle ID in your project settings, under General)&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; file and add it to your app.&lt;/li&gt;
&lt;li&gt;Import Firebase to your AppDelegate, and call &lt;code&gt;FirebaseApp.configure()&lt;/code&gt; in &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run your application and have Firebase verify communication.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will then be presented with the Firebase dashboard. Go to the Develop pane, where you'll find the Authentication section.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2Aq7JkjCxm3jNRV4xWYEVPNQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2Aq7JkjCxm3jNRV4xWYEVPNQ.png" alt="URL Schemes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;AppDelegate.swift&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import FirebaseUI

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

    return false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;There are plenty of other sign-in options that you may want to allow, but we won't be covering them here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Setting up the View
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2APhup8scjoRQitSQqRJSx4Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2APhup8scjoRQitSQqRJSx4Q.png" alt="Storyboard Layout"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class VideoCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var videoView: UIView!    
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll also need a bunch of buttons to handle all the user's actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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) {

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Logging in with FirebaseUI
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;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 &lt;a href="https://firebase.google.com/docs/auth/ios/password-auth?authuser=0" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://firebase.google.com/docs/auth/ios/google-signin?authuser=0" rel="noopener noreferrer"&gt;here&lt;/a&gt;, respectively.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We're going to be using FirebaseUI to log in to our app. We'll have our initial entry screen - our &lt;code&gt;AgoraVideoViewController&lt;/code&gt; - 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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?) {

    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;AuthStateDidChangeListener&lt;/code&gt;. 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a functional login page that will appear if the current user is nil.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating a User Database
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2APHaHVPWSmXLR7YmvgpwgXQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2APHaHVPWSmXLR7YmvgpwgXQ.png" alt="Creating a Realtime Database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding users on login
&lt;/h4&gt;

&lt;p&gt;Head back to our &lt;code&gt;FUIAuthDelegate&lt;/code&gt; extension. We're going to make use of that &lt;code&gt;didSignInWith&lt;/code&gt; callback to add a user to our database whenever they log in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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])
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Joining a Video Call
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Add Camera and Microphone Permissions
&lt;/h4&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Privacy - Microphone Usage Description
Privacy - Camera Usage Description
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Initialize the Agora Engine
&lt;/h4&gt;

&lt;p&gt;In order to do anything with Agora, we need an &lt;code&gt;AgoraRtcEngineKit&lt;/code&gt; object initialized with our appID (acquired from the &lt;a href="https://console.agora.io" rel="noopener noreferrer"&gt;Agora Developer Console&lt;/a&gt;). Let's add a helper function that will give us this object when we need it, creating it first if necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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() -&amp;gt; AgoraRtcEngineKit {
    if agoraKit == nil {
        agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: appID, delegate: self)
    }
    return agoraKit!
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Enable Video
&lt;/h4&gt;

&lt;p&gt;Next, we need to tell Agora we want to enable the video, and set up the video configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Join a Call
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Update your &lt;code&gt;viewWillAppear&lt;/code&gt; and add a new function for joining a call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Displaying Video
&lt;/h4&gt;

&lt;p&gt;It's finally time to put our &lt;code&gt;UICollectionView&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we need to set up our collection view cells to display video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -&amp;gt; Int {
    return remoteUserIDs.count + 1
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&amp;gt; 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) -&amp;gt; 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)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: Make sure you set your view controller as a UICollectionViewDelegateFlowLayout. Learn from my mistakes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Working With Friends
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Searching for Users
&lt;/h4&gt;

&lt;p&gt;We're going to create a &lt;code&gt;UserSearchViewController&lt;/code&gt; to search through our user database for possible new friends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AVx7nMXaYw0gD08j83RLwOg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AVx7nMXaYw0gD08j83RLwOg.png" alt="UserSearchViewController"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need a UISearchBar, and a UITableView to display the results. We'll make a very simple UserTableViewCell class for our cells:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;AgoraVideoViewController&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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) {

    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set up some basic stuff when we load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of our complex logic comes when the user actually searches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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) -&amp;gt; Int {
    return resultsArray.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&amp;gt; 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
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user hits the search button, we perform a database query to return all users whose username starts with the text entered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[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&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;.indexOn&lt;/code&gt; field to your users database and hit Publish:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AaljlIndKpB_GtA2ca2Gyxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AaljlIndKpB_GtA2ca2Gyxg.png" alt="Indexing on Username"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding a Friend
&lt;/h4&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Viewing Your Friends
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A_6zN3WYCEo0WeM1Y0IjAJA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A_6zN3WYCEo0WeM1Y0IjAJA.png" alt="Join Friend"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;AgoraVideoViewController&lt;/code&gt; which friend's room we want to join.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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) {

    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in our new class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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")
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the view appears, we'll get our usual reference to the current user, and use that to get each friend's ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we display each friend's username, and pass the ID back to the video view when selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&amp;gt; Int {
    return resultsArray.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&amp;gt; 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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, back in our &lt;code&gt;AgoraVideoViewController&lt;/code&gt; we handle the message and change rooms accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Locking Rooms
&lt;/h3&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line to the success callback of &lt;code&gt;joinChannel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self?.isLocalCall = channelName == self?.currentUser?.uid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then implement the handler for the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;JoinFriendViewController&lt;/code&gt; let's first show the user if a room is locked by adding the following to &lt;code&gt;cellForRowAt:&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike with &lt;code&gt;observeSingleEvent&lt;/code&gt;, the &lt;code&gt;observe&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;We can then update our &lt;code&gt;didSelectRowAt&lt;/code&gt; function to show an alert if the user tries to join a locked room:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
      }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it!&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Text Chat
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the chat room itself, we’ll need one final view controller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A7uvEQR2DHExLi2QpY9IMGg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A7uvEQR2DHExLi2QpY9IMGg.png" alt="Chat View Controller"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need a UITextField to send messages, and a table view to display them. We again have a very simple cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ChatTableViewCell: UITableViewCell {

    @IBOutlet weak var messageLabel: UILabel!

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like before, we’re going to present this view as a popover when the user hits the ‘Chat’ button.&lt;/p&gt;

&lt;h4&gt;
  
  
  Joining a Text Channel
&lt;/h4&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Showing Messages
&lt;/h4&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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) -&amp;gt; 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) -&amp;gt; Int {
    messageList.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&amp;gt; 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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Handling the Keyboard
&lt;/h4&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Finishing Touches
&lt;/h4&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@IBAction func didTapSwitchCamera(_ sender: Any) {
    getAgoraEngine().switchCamera()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to mute our local audio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Congratulations
&lt;/h4&gt;

&lt;p&gt;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 &lt;a href="https://webdemo.agora.io/agora-web-showcase/" rel="noopener noreferrer"&gt;web demo&lt;/a&gt; to substitute as additional users.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="mailto:devrel@agora.io"&gt;devrel@agora.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>agoraio</category>
    </item>
  </channel>
</rss>
