video sdk logo

Login

iOS Video Calling Tutorial

Add real-time 1:1 and group video calling to any iOS app in Swift. SwiftUI components, CallKit integration for the system call UI, and Picture-in-Picture support, all built on the Video & Audio SDK.
iOS video calling app built with VideoSDK in SwiftUI showing a participant grid and call controls

Trusted by 300+ global business & tech leaders

Build a Video Call App in Swift, Step by Step

Learn how to add video calling to an iOS app using the VideoSDK iOS SDK. This step-by-step tutorial covers CocoaPods and Swift Package Manager setup, camera and microphone permissions, participant rendering with VideoView, and call controls in Swift.


Prerequisites

Before you begin, make sure you have:

  • iOS 11.0+
  • Xcode 12.0+
  • Swift 5.0+
  • A VideoSDK developer account
  • CocoaPods installed, for running pod init and pod install
  • An auth token, generated with the videosdk-server-api-example or from the VideoSDK Dashboard

Production note: A temporary dashboard token is suitable only for development and testing. In production, generate short-lived access tokens on a secure server. Never ship your VideoSDK API secret inside an app binary or a public repository.

App Architecture

This app will contain two screens:

  1. Join Screen : This screen allows the user to either create a meeting or join a predefined meeting.

  2. Meeting Screen : This screen basically contains the local and remote participant view and some meeting controls such as Enable / Disable Mic & Camera and Leave meeting.

VideoSDK iOS quickstart architecture showing the Join Screen and Meeting Screen with local and remote participant views

Getting Started With the Code!

This tutorial builds the app in small, testable pieces. You will create the Xcode project, install the SDK with CocoaPods, add the data models and API client, and then implement the join screen, the meeting screen, the call controls, and the two event listeners that drive the UI.

Step 1: Create App

Step 1: Create a new application by selecting Create a new Xcode project

Step 2: Choose App then click Next

Xcode template chooser with the App template selected for the VideoSDK iOS quickstart

Step 3: Add Product Name and Save the project.

Xcode new project sheet showing the Product Name field for the VideoSDK iOS quickstart

Step 2: VideoSDK Installation

To install VideoSDK, you must initialise the pod on the project by running the following command.

Terminal
1pod init
2

It will create the Podfile in your project folder, open that file and add the dependency for the VideoSDK like below:

Podfile
1pod 'VideoSDKRTC', :git => 'https://github.com/videosdk-live/videosdk-rtc-ios-sdk.git'
2

Podfile in Xcode with the VideoSDKRTC pod dependency added

then run the below code to install the pod:

Terminal
1pod install
2

then declare the permissions in Info.plist :

Info.plist
1<key>NSCameraUsageDescription</key>
2<string>Camera permission description</string>
3<key>NSMicrophoneUsageDescription</key>
4<string>Microphone permission description</string>
5

Step 3: Project Structure

Project Structure
1iOSQuickStartDemo
2   ├── Models
3        ├── RoomStruct.swift
4        └── MeetingData.swift
5   ├── ViewControllers
6        ├── StartMeetingViewController.swift
7        └── MeetingViewController.swift
8   ├── AppDelegate.swift // Default
9   ├── SceneDelegate.swift // Default
10   └── APIService
11           └── APIService.swift
12   ├── Main.storyboard // Default
13   ├── LaunchScreen.storyboard // Default
14   └── Info.plist // Default
15 Pods
16     └── Podfile
17

Main.storyboard Design

Main.storyboard layout for the VideoSDK iOS quickstart showing the join screen and meeting screen scenes

Wire the outlets and actions used in Steps 7 and 8 to the views in this storyboard. The code will not run until each @IBOutlet and @IBAction is connected.

Step 4: Create Models

Create swift file for MeetingData and RoomStruct class model for setting data in object pattern.

MeetingData.swift
1import Foundation
2
3struct MeetingData {
4    let token: String
5    let name: String
6    let meetingId: String
7    let micEnabled: Bool
8    let cameraEnabled: Bool
9}
10
RoomStruct.swift
1import Foundation
2
3struct RoomsStruct: Codable {
4    let createdAt, updatedAt, roomID: String?
5    let links: Links?
6    let id: String?
7
8    enum CodingKeys: String, CodingKey {
9        case createdAt, updatedAt
10        case roomID = "roomId"
11        case links, id
12    }
13}
14
15// MARK: - Links
16struct Links: Codable {
17    let getRoom, getSession: String?
18
19    enum CodingKeys: String, CodingKey {
20        case getRoom = "get_room"
21        case getSession = "get_session"
22    }
23}
24

MeetingData carries the token, participant name, meeting ID, and the initial mic and camera state from the join screen to the meeting screen. RoomsStruct decodes the response from the VideoSDK Rooms API.

Step 5: Get Started with APIClient

Before jumping to anything else, we have to write API to generate unique meetingId. You will require Auth token, you can generate it using either videosdk-server-api-example or generate it from the VideoSDK Dashboard for developer.

APIService.swift
1import Foundation
2
3let TOKEN_STRING: String = "<AUTH_TOKEN>";
4
5class APIService {
6
7    class func createMeeting(token: String, completion: @escaping (Result<String, Error>) -> Void) {
8        let url = URL(string: "https://api.videosdk.live/v2/rooms")!
9
10        var request = URLRequest(url: url)
11        request.httpMethod = "POST"
12        request.addValue(TOKEN_STRING, forHTTPHeaderField: "authorization")
13
14        URLSession.shared.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
15            DispatchQueue.main.async {
16                if let data = data, let utf8Text = String(data: data, encoding: .utf8)
17                {
18                    do{
19                        let dataArray = try JSONDecoder().decode(RoomsStruct.self,from: data)
20                        completion(.success(dataArray.roomID ?? ""))
21                    } catch {
22                        print("Error while creating a meeting: \(error)")
23                        completion(.failure(error))
24                    }
25                }
26            }
27        }).resume()
28    }
29}
30

Step 6: Implement Join Screen

Join screen will work as medium to either schedule new meeting or to join existing meeting.

StartMeetingViewController.swift
1import Foundation
2import UIKit
3
4class StartMeetingViewController: UIViewController, UITextFieldDelegate {
5
6        private var serverToken = ""
7
8        /// MARK: outlet for create meeting button
9        @IBOutlet weak var btnCreateMeeting: UIButton!
10
11        /// MARK: outlet for join meeting button
12        @IBOutlet weak var btnJoinMeeting: UIButton!
13
14        /// MARK: outlet for meetingId textfield
15        @IBOutlet weak var txtMeetingId: UITextField!
16
17        /// MARK: Initialize the private variable with TOKEN_STRING &
18        /// setting the meeting id in the textfield
19        override func viewDidLoad() {
20            txtMeetingId.delegate = self
21            serverToken = TOKEN_STRING
22            txtMeetingId.text = "PROVIDE-STATIC-MEETING-ID"
23        }
24
25        /// MARK: method for joining meeting through seague named as "StartMeeting"
26        /// after validating the serverToken in not empty
27        func joinMeeting() {
28            txtMeetingId.resignFirstResponder()
29
30            if !serverToken.isEmpty {
31                DispatchQueue.main.async {
32                    self.dismiss(animated: true) {
33                        self.performSegue(withIdentifier: "StartMeeting", sender: nil)
34                    }
35                }
36            } else {
37                print("Please provide auth token to start the meeting.")
38            }
39        }
40
41        /// MARK: outlet for create meeting button tap event
42        @IBAction func btnCreateMeetingTapped(_ sender: Any) {
43                print("show loader while meeting gets connected with server")
44            joinRoom()
45        }
46
47        /// MARK: outlet for join meeting button tap event
48        @IBAction func btnJoinMeetingTapped(_ sender: Any) {
49            if((txtMeetingId.text ?? "").isEmpty){
50
51                        print("Please provide meeting id to start the meeting.")
52                txtMeetingId.resignFirstResponder()
53            } else {
54                joinMeeting()
55            }
56        }
57
58        // MARK: - method for creating room api call and getting meetingId for joining meeting
59
60        func joinRoom() {
61
62           APIService.createMeeting(token: self.serverToken) { result in
63            if case .success(let meetingId) = result {
64                DispatchQueue.main.async {
65                    self.txtMeetingId.text = meetingId
66                    self.joinMeeting()
67                }
68            }
69        }
70        }
71
72        /// MARK: preparing to animate to meetingViewController screen
73        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
74
75            guard let navigation = segue.destination as? UINavigationController,
76                  let meetingViewController = navigation.topViewController as? MeetingViewController else {
77                  return
78              }
79
80            meetingViewController.meetingData = MeetingData(
81                token: serverToken,
82                name: txtMeetingId.text ?? "Guest",
83                meetingId: txtMeetingId.text ?? "",
84                micEnabled: true,
85                cameraEnabled: true
86            )
87        }
88   }     
89 

Output ios_quickstart_join_screen.png

Step 7: Initialize and Join Meeting

Using the provided token and meetingId, we will configure and initialise the meeting in viewDidLoad().

Then, we'll add @IBOutlet for localParticipantVideoView and remoteParticipantVideoView, which can render local and remote participant media respectively.

MeetingViewController.swift
1import UIKit
2import VideoSDKRTC
3import WebRTC
4import AVFoundation
5
6class MeetingViewController: UIViewController {
7
8    // MARK: - Properties
9
10    // outlet for local participant container view
11    @IBOutlet weak var localParticipantViewContainer: UIView!
12
13    // outlet for label for meeting Id
14    @IBOutlet weak var lblMeetingId: UILabel!
15
16    // outlet for local participant video view
17    @IBOutlet weak var localParticipantVideoView: RTCMTLVideoView!
18
19    // outlet for remote participant video view
20    @IBOutlet weak var remoteParticipantVideoView: RTCMTLVideoView!
21
22    // outlet for remote participant no media label
23    @IBOutlet weak var lblRemoteParticipantNoMedia: UILabel!
24
25    // outlet for remote participant container view
26    @IBOutlet weak var remoteParticipantViewContainer: UIView!
27
28    // outlet for local participant no media label
29    @IBOutlet weak var lblLocalParticipantNoMedia: UILabel!
30
31    /// Meeting data - required to start
32    var meetingData: MeetingData!
33
34    /// current meeting reference
35    private var meeting: Meeting?
36
37    // MARK: - video participants including self to show in UI
38    private var participants: [Participant] = []
39
40    // MARK: - Lifecycle Events
41
42    override func viewDidLoad() {
43        super.viewDidLoad()
44        // configure the VideoSDK with token
45        VideoSDK.config(token: meetingData.token)
46
47        // init meeting
48        initializeMeeting()
49
50        // set meeting id in button text
51        lblMeetingId.text = "Meeting Id: \(meetingData.meetingId)"
52    }
53
54    override func viewWillAppear(_ animated: Bool) {
55        super.viewWillAppear(animated)
56        navigationController?.navigationBar.isHidden = true
57    }
58
59    override func viewWillDisappear(_ animated: Bool) {
60        super.viewWillDisappear(animated)
61        navigationController?.navigationBar.isHidden = false
62        NotificationCenter.default.removeObserver(self)
63    }
64
65    // MARK: - Meeting
66
67    private func initializeMeeting() {
68
69        // Initialize the VideoSDK
70        meeting = VideoSDK.initMeeting(
71            meetingId: meetingData.meetingId,
72            participantName: meetingData.name,
73            micEnabled: meetingData.micEnabled,
74            webcamEnabled: meetingData.cameraEnabled
75        )
76
77        // Adding the listener to meeting
78        meeting?.addEventListener(self)
79
80        // joining the meeting
81        meeting?.join()
82    }
83}
84

micEnabled and webcamEnabled are read from the MeetingData object passed in by the join screen, so the participant publishes camera and microphone media as soon as join() succeeds.

Step 8: Implement Controls

After initialising the meeting in the previous step, we will now add @IBOutlet for btnLeave, btnToggleVideo and btnToggleMic which can control the media in the meeting.

In the block below, ... marks the code you already wrote in Step 7. Add these members inside the same MeetingViewController class.

MeetingViewController.swift
1class MeetingViewController: UIViewController {
2
3    ...
4
5    // outlet for leave button
6    @IBOutlet weak var btnLeave: UIButton!
7
8    // outlet for toggle video button
9    @IBOutlet weak var btnToggleVideo: UIButton!
10
11    // outlet for toggle audio button
12    @IBOutlet weak var btnToggleMic: UIButton!
13
14    // bool for mic
15    var micEnabled = true
16    // bool for video
17    var videoEnabled = true
18
19    // outlet for leave button click event
20    @IBAction func btnLeaveTapped(_ sender: Any) {
21        DispatchQueue.main.async {
22            self.meeting?.leave()
23            self.dismiss(animated: true)
24        }
25    }
26
27    // outlet for toggle mic button click event
28    @IBAction func btnToggleMicTapped(_ sender: Any) {
29        if micEnabled {
30            micEnabled = !micEnabled // false
31            self.meeting?.muteMic()
32        } else {
33            micEnabled = !micEnabled // true
34            self.meeting?.unmuteMic()
35        }
36    }
37
38    // outlet for toggle video button click event
39    @IBAction func btnToggleVideoTapped(_ sender: Any) {
40        if videoEnabled {
41            videoEnabled = !videoEnabled // false
42            self.meeting?.disableWebcam()
43        } else {
44            videoEnabled = !videoEnabled // true
45            self.meeting?.enableWebcam()
46        }
47    }
48
49    ...
50
51}
52

Output

VideoSDK iOS meeting screen in its loading state before media streams arrive

Step 9: Implementing MeetingEventListener

In this step, we'll create an extension for the MeetingViewController that implements the MeetingEventListener, which implements the onMeetingJoined, onMeetingLeft, onParticipantJoined, onParticipantLeft, onParticipantChanged, onSpeakerChanged, etc. methods.

Add this extension at file scope in MeetingViewController.swift, below the closing brace of the class.

MeetingViewController.swift
1extension MeetingViewController: MeetingEventListener {
2
3    /// Meeting started
4    func onMeetingJoined() {
5
6        // handle local participant on start
7        guard let localParticipant = self.meeting?.localParticipant else { return }
8        // add to list
9        participants.append(localParticipant)
10
11        // add event listener
12        localParticipant.addEventListener(self)
13
14        localParticipant.setQuality(.high)
15
16        if(localParticipant.isLocal){
17            self.localParticipantViewContainer.isHidden = false
18        } else {
19            self.remoteParticipantViewContainer.isHidden = false
20        }
21    }
22
23    /// Meeting ended
24    func onMeetingLeft() {
25        // remove listeners
26        meeting?.localParticipant.removeEventListener(self)
27        meeting?.removeEventListener(self)
28    }
29
30    /// A new participant joined
31    func onParticipantJoined(_ participant: Participant) {
32        participants.append(participant)
33
34        // add listener
35        participant.addEventListener(self)
36
37        participant.setQuality(.high)
38
39        if(participant.isLocal){
40            self.localParticipantViewContainer.isHidden = false
41        } else {
42            self.remoteParticipantViewContainer.isHidden = false
43        }
44    }
45
46    /// A participant left from the meeting
47    /// - Parameter participant: participant object
48    func onParticipantLeft(_ participant: Participant) {
49        participant.removeEventListener(self)
50        guard let index = self.participants.firstIndex(where: { $0.id == participant.id }) else {
51            return
52        }
53        // remove participant from list
54        participants.remove(at: index)
55        // hide from ui
56        UIView.animate(withDuration: 0.5){
57            if(!participant.isLocal){
58                self.remoteParticipantViewContainer.isHidden = true
59            }
60        }
61    }
62
63    /// Called when speaker is changed
64    /// - Parameter participantId: participant id of the speaker, nil when no one is speaking.
65    func onSpeakerChanged(participantId: String?) {
66
67        // show indication for active speaker
68        if let participant = participants.first(where: { $0.id == participantId }) {
69            self.showActiveSpeakerIndicator(participant.isLocal ? localParticipantViewContainer : remoteParticipantViewContainer, true)
70        }
71
72        // hide indication for others participants
73        let otherParticipants = participants.filter { $0.id != participantId }
74        for participant in otherParticipants {
75            if participants.count > 1 && participant.isLocal {
76                showActiveSpeakerIndicator(localParticipantViewContainer, false)
77            } else {
78                showActiveSpeakerIndicator(remoteParticipantViewContainer, false)
79            }
80        }
81    }
82
83    func showActiveSpeakerIndicator(_ view: UIView, _ show: Bool) {
84        view.layer.borderWidth = 4.0
85        view.layer.borderColor = show ? UIColor.blue.cgColor : UIColor.clear.cgColor
86    }
87
88}
89

Step 10: Implementing ParticipantEventListener

In this stage, we'll add an extension for the MeetingViewController that implements the ParticipantEventListener, which implements the onStreamEnabled and onStreamDisabled methods for the audio and video of MediaStreams enabled or disabled.

The function updateUI is frequently used to control or modify the user interface (enable / disable camera & mic) in accordance with MediaStream state.

MeetingViewController.swift
1extension MeetingViewController: ParticipantEventListener {
2
3    /// Participant has enabled mic, video or screenshare
4    /// - Parameters:
5    ///   - stream: enabled stream object
6    ///   - participant: participant object
7    func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
8        updateUI(participant: participant, forStream: stream, enabled: true)
9    }
10
11    /// Participant has disabled mic, video or screenshare
12    /// - Parameters:
13    ///   - stream: disabled stream object
14    ///   - participant: participant object
15    func onStreamDisabled(_ stream: MediaStream, forParticipant participant: Participant) {
16        updateUI(participant: participant, forStream: stream, enabled: false)
17    }
18}
19
20private extension MeetingViewController {
21
22    func updateUI(participant: Participant, forStream stream: MediaStream, enabled: Bool) { // true
23        switch stream.kind {
24        case .state(value: .video):
25            if let videotrack = stream.track as? RTCVideoTrack {
26                if enabled {
27                    DispatchQueue.main.async {
28                        UIView.animate(withDuration: 0.5){
29                            if(participant.isLocal){
30                                self.localParticipantViewContainer.isHidden =   false
31                                self.localParticipantVideoView.isHidden = false
32                                self.localParticipantVideoView.videoContentMode = .scaleAspectFill
33                                self.localParticipantViewContainer.bringSubviewToFront(self.localParticipantVideoView)
34                                videotrack.add(self.localParticipantVideoView)
35                                self.lblLocalParticipantNoMedia.isHidden = true
36                            } else {
37                                self.remoteParticipantViewContainer.isHidden = false
38                                self.remoteParticipantVideoView.isHidden = false
39                                self.remoteParticipantVideoView.videoContentMode = .scaleAspectFill
40                                self.remoteParticipantViewContainer.bringSubviewToFront(self.remoteParticipantVideoView)
41                                videotrack.add(self.remoteParticipantVideoView)
42                                self.lblRemoteParticipantNoMedia.isHidden = true
43                            }
44                        }
45                    }
46                } else {
47                    UIView.animate(withDuration: 0.5){
48                        if(participant.isLocal){
49                            self.localParticipantViewContainer.isHidden = false
50                            self.localParticipantVideoView.isHidden = true
51                            self.lblLocalParticipantNoMedia.isHidden = false
52                            videotrack.remove(self.localParticipantVideoView)
53                        } else {
54                            self.remoteParticipantViewContainer.isHidden = false
55                            self.remoteParticipantVideoView.isHidden = true
56                            self.lblRemoteParticipantNoMedia.isHidden = false
57                            videotrack.remove(self.remoteParticipantVideoView)
58                        }
59                    }
60                }
61            }
62
63        case .state(value: .audio):
64            if participant.isLocal {
65                localParticipantViewContainer.layer.borderWidth = 4.0
66                localParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
67            } else {
68                remoteParticipantViewContainer.layer.borderWidth = 4.0
69                remoteParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
70            }
71        default:
72            break
73        }
74    }
75}
76

Output

VideoSDK iOS meeting screen showing local and remote participant video with mic and camera controls

Known Issue

Please add the following line in the MeetingViewController.swift file's viewDidLoad method If you get your video out of the container view like below image.

VideoSDK iOS known issue where the participant video renders outside its container view

MeetingViewController.swift
1override func viewDidLoad() {
2
3localParticipantVideoView.frame = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)
4
5localParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)
6
7localParticipantVideoView.clipsToBounds = true
8
9remoteParticipantVideoView.frame = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
10remoteParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
11remoteParticipantVideoView.clipsToBounds = true
12}
13

Final Output

Build and run the app from Xcode, and allow camera and microphone access when iOS asks for permission.

To test the call:

  1. Create a meeting from the Join Screen on the first device.
  2. Copy the meeting ID shown in lblMeetingId on the Meeting Screen.
  3. Run the app on a second device.
  4. Enter the same meeting ID on the Join Screen.
  5. Join the meeting.
  6. Allow camera and microphone permission on both devices.

Your completed iOS app can now:

  • Create a new VideoSDK room
  • Join an existing room by meeting ID
  • Publish camera and microphone media
  • Render local and remote participant video
  • Show an active speaker indicator
  • Show a red border when a participant's microphone is disabled
  • Toggle the local microphone
  • Toggle the local camera
  • Leave the meeting and dismiss the meeting screen

Tip: Stuck anywhere? Check out this example code on GitHub. After the basic call works, explore recording, screen sharing, participant events, and other iOS SDK features in the VideoSDK documentation.

Summarize with

ChatGPTClaudePerplexityGoogleGrok
Prefer VideoSDK on Google

Table of Contents

Start Building Now With $20 Free Balance

No credit card required to start.

Documentation →Github Repo →

Share This Article

TwitterLinkedInFacebookHacker News

Get started for free today

Grab your API keys and start building with free $20 included in your account. Need enterprise-grade security or custom workloads? Talk to an expert!

Book a demo

Video SDK Logo

United States

28 Geary St, Suite 650,
San Francisco, CA 94108, United States

India flag

India

18th Floor, 1812, The Junomoneta Tower,
Adajan-Hazira Rd, Surat, Gujarat 395009, India

Video SDK Logo
Video SDK Logo
Video SDK Logo
Video SDK Logo
SOLUTIONS

Video KYC

Video Banking

Virtual Claim

Video MER

Telehealth

Astrology

Gaming

Dating

Live Commerce

Auto Proctoring

Interview-as-a-service

Virtual Events

Live Audio Streaming

Ed-Tech

PRODUCTS

AI-Agents

Real-time Audio & Video SDK

Interactive Live Streaming SDK

Real-time Transcription SDK

Character SDK

Open Source Examples

SUCCESS STORIES

Examedi

Coderschool

TYHO

ForagerOne

Immigo

DEVELOPERS

Documentation

Code Samples

Developer Updates

Developer Hub

TOP ARTICLES

What is WebRTC?

Build a React Native Video Calling App

Build a Flutter Video Calling App

RESOURCES

The Protocol by Video SDK

AI Apps

Creator Program

LEGAL

Terms Of Service

Privacy Policy

Cookie Notice

CCPA Notice

Subprocessors

DPA

RSS

COMPANY

Contact Us

Pricing

Support

Blog

Press Kit