Introduction

This tutorial is about creating a video call app using the iOS platform. You will use Xcode(an IDE for iOS app development) and Swift(a programming language).

The tutorial will guide you through setting up the project, integrating the VideoSDK library with CocoaPods, and implementing the joining screen and meeting functionalities.

By the end, you will have developed a video calling app, learned iOS development, and gained skills in integrating external libraries and handling meeting interactions. This will enable you to create similar apps or enhance existing ones with video-calling capabilities.

Prerequisites

App Architecture

This App will contain two screens:

  1. Join Screen: This screen allows the user to either create a meeting or join the predefined meeting.
  2. Meeting Screen: This screen basically contains local and remote participant views and some meeting controls such as Enable / Disable Mic & Camera and Leaving the meeting.

Video SDK Image

Getting Started With the Code!

Create App

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

Step 2: Choose App then click Next

Video SDK Image

Step 3: Add the Product Name and Save the project.

Video SDK Image

VideoSDK Installation

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

pod init

Video SDK Image

then run the below code to install the pod:

pod install

then declare the permissions in Info.plist :

<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>

Project Structure

iOSQuickStartDemo
   ├── Models
        ├── RoomStruct.swift
        └── MeetingData.swift
   ├── ViewControllers
        ├── StartMeetingViewController.swift
        └── MeetingViewController.swift
   ├── AppDelegate.swift // Default
   ├── SceneDelegate.swift // Default
   └── APIService
           └── APIService.swift
   ├── Main.storyboard // Default
   ├── LaunchScreen.storyboard // Default
   └── Info.plist // Default
 Pods
     └── Podfile

Main.storyboard Design

Video SDK Image

Create models

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

MeetingData.swift

import Foundation
struct MeetingData {
    let token: String
    let name: String
    let meetingId: String
    let micEnabled: Bool
    let cameraEnabled: Bool
}

RoomStruct.swift

import Foundation
struct RoomsStruct: Codable {
    let createdAt, updatedAt, roomID: String?
    let links: Links?
    let id: String?
    enum CodingKeys: String, CodingKey {
        case createdAt, updatedAt
        case roomID = "roomId"
        case links, id
    }
}
// MARK: - Links
struct Links: Codable {
    let getRoom, getSession: String?
    enum CodingKeys: String, CodingKey {
        case getRoom = "get_room"
        case getSession = "get_session"
    }
}

5 Steps to Build IOS Video Call App

Step 1: Get started with APIClient

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

APIService.swift

import Foundation

let TOKEN_STRING: String = "<AUTH_TOKEN>";

class APIService {

    class func createMeeting(token: String, completion: @escaping (Result<String, Error>) -> Void) {
        let url = URL(string: "https://api.videosdk.live/v2/rooms")!

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue(TOKEN_STRING, forHTTPHeaderField: "authorization")

        URLSession.shared.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
            DispatchQueue.main.async {
                if let data = data, let utf8Text = String(data: data, encoding: .utf8)
                {
                    do{
                        let dataArray = try JSONDecoder().decode(RoomsStruct.self,from: data)
                        completion(.success(dataArray.roomID ?? ""))
                    } catch {
                        print("Error while creating a meeting: \(error)")
                        completion(.failure(error))
                    }
                }
            }
        }).resume()
    }
}

Step 2: Implement Join Screen

The join screen will work as a medium to either schedule a new meeting or join the existing meeting.

StartMeetingViewController.swift

import Foundation
import UIKit

class StartMeetingViewController: UIViewController, UITextFieldDelegate {

        private var serverToken = ""

        /// MARK: outlet for create meeting button
        @IBOutlet weak var btnCreateMeeting: UIButton!

        /// MARK: outlet for join meeting button
        @IBOutlet weak var btnJoinMeeting: UIButton!

        /// MARK: outlet for meetingId textfield
        @IBOutlet weak var txtMeetingId: UITextField!

        /// MARK: Initialize the private variable with TOKEN_STRING &
        /// setting the meeting id in the textfield
        override func viewDidLoad() {
            txtMeetingId.delegate = self
            serverToken = TOKEN_STRING
            txtMeetingId.text = "PROVIDE-STATIC-MEETING-ID"
        }

        /// MARK: method for joining meeting through seague named as "StartMeeting"
        /// after validating the serverToken in not empty
        func joinMeeting() {
            txtMeetingId.resignFirstResponder()

            if !serverToken.isEmpty {
                DispatchQueue.main.async {
                    self.dismiss(animated: true) {
                        self.performSegue(withIdentifier: "StartMeeting", sender: nil)
                    }
                }
            } else {
                print("Please provide auth token to start the meeting.")
            }
        }

        /// MARK: outlet for create meeting button tap event
        @IBAction func btnCreateMeetingTapped(_ sender: Any) {
                print("show loader while meeting gets connected with server")
            joinRoom()
        }

        /// MARK: outlet for join meeting button tap event
        @IBAction func btnJoinMeetingTapped(_ sender: Any) {
            if((txtMeetingId.text ?? "").isEmpty){

                        print("Please provide meeting id to start the meeting.")
                txtMeetingId.resignFirstResponder()
            } else {
                joinMeeting()
            }
        }

        // MARK: - method for creating room api call and getting meetingId for joining meeting

        func joinRoom() {

           APIService.createMeeting(token: self.serverToken) { result in
            if case .success(let meetingId) = result {
                DispatchQueue.main.async {
                    self.txtMeetingId.text = meetingId
                    self.joinMeeting()
                }
            }
        }
        }

        /// MARK: preparing to animate to meetingViewController screen
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

            guard let navigation = segue.destination as? UINavigationController,
                  let meetingViewController = navigation.topViewController as? MeetingViewController else {
                  return
              }

            meetingViewController.meetingData = MeetingData(
                token: serverToken,
                name: txtMeetingId.text ?? "Guest",
                meetingId: txtMeetingId.text ?? "",
                micEnabled: true,
                cameraEnabled: true
            )
        }
}

Output

Video SDK Image

Step 3: Initialize and Join Meeting

Using the provided token and meetingId, You will configure and initialize the meeting in viewDidLoad().

Then, you need to add @IBOutlet for localParticipantVideoView and remoteParticipantVideoView, which can render local and remote participant media respectively.

MeetingViewController.swift


class MeetingViewController: UIViewController {

import UIKit
import VideoSDKRTC
import WebRTC
import AVFoundation

class MeetingViewController: UIViewController {

    // MARK: - Properties
    // outlet for local participant container view
    @IBOutlet weak var localParticipantViewContainer: UIView!

    // outlet for label for meeting Id
    @IBOutlet weak var lblMeetingId: UILabel!

    // outlet for local participant video view
    @IBOutlet weak var localParticipantVideoView: RTCMTLVideoView!

    // outlet for remote participant video view
    @IBOutlet weak var remoteParticipantVideoView: RTCMTLVideoView!

    // outlet for remote participant no media label
    @IBOutlet weak var lblRemoteParticipantNoMedia: UILabel!

    // outlet for remote participant container view
    @IBOutlet weak var remoteParticipantViewContainer: UIView!

    // outlet for local participant no media label
    @IBOutlet weak var lblLocalParticipantNoMedia: UILabel!

    /// Meeting data - required to start
    var meetingData: MeetingData!

    /// current meeting reference
    private var meeting: Meeting?

    // MARK: - video participants including self to show in UI
    private var participants: [Participant] = []

        // MARK: - Lifecycle Events

        override func viewDidLoad() {
        super.viewDidLoad()
        // configure the VideoSDK with token
        VideoSDK.config(token: meetingData.token)

        // init meeting
        initializeMeeting()

        // set meeting id in button text
        lblMeetingId.text = "Meeting Id: \(meetingData.meetingId)"
      }

      override func viewWillAppear(_ animated: Bool) {
          super.viewWillAppear(animated)
          navigationController?.navigationBar.isHidden = true
      }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        navigationController?.navigationBar.isHidden = false
        NotificationCenter.default.removeObserver(self)
    }

        // MARK: - Meeting

        private func initializeMeeting() {

            // Initialize the VideoSDK
            meeting = VideoSDK.initMeeting(
                meetingId: meetingData.meetingId,
                participantName: meetingData.name,
                micEnabled: meetingData.micEnabled,
                webcamEnabled: meetingData.cameraEnabled
            )

            // Adding the listener to meeting
            meeting?.addEventListener(self)

            // joining the meeting
            meeting?.join()
        }
}

Step 4: Implement Controls

After initializing the meeting in the previous step. You now need to add @IBOutlet for btnLeave, btnToggleVideo and btnToggleMic that can control media in the meeting.

MeetingViewController.swift


class MeetingViewController: UIViewController {

...

    // outlet for leave button
    @IBOutlet weak var btnLeave: UIButton!

    // outlet for toggle video button
    @IBOutlet weak var btnToggleVideo: UIButton!

    // outlet for toggle audio button
    @IBOutlet weak var btnToggleMic: UIButton!

    // bool for mic
    var micEnabled = true
    // bool for video
    var videoEnabled = true


    // outlet for leave button click event
    @IBAction func btnLeaveTapped(_ sender: Any) {
            DispatchQueue.main.async {
                self.meeting?.leave()
                self.dismiss(animated: true)
            }
        }

    // outlet for toggle mic button click event
    @IBAction func btnToggleMicTapped(_ sender: Any) {
        if micEnabled {
            micEnabled = !micEnabled // false
            self.meeting?.muteMic()
        } else {
            micEnabled = !micEnabled // true
            self.meeting?.unmuteMic()
        }
    }

    // outlet for toggle video button click event
    @IBAction func btnToggleVideoTapped(_ sender: Any) {
        if videoEnabled {
            videoEnabled = !videoEnabled // false
            self.meeting?.disableWebcam()
        } else {
            videoEnabled = !videoEnabled // true
            self.meeting?.enableWebcam()
        }
    }

...

}

Output

Video SDK Image

Step 5 : Implementing MeetingEventListener

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

MeetingViewController.swift


class MeetingViewController: UIViewController {

...

extension MeetingViewController: MeetingEventListener {

        /// Meeting started
        func onMeetingJoined() {

            // handle local participant on start
            guard let localParticipant = self.meeting?.localParticipant else { return }
            // add to list
            participants.append(localParticipant)

            // add event listener
            localParticipant.addEventListener(self)

            localParticipant.setQuality(.high)

            if(localParticipant.isLocal){
                self.localParticipantViewContainer.isHidden = false
            } else {
                self.remoteParticipantViewContainer.isHidden = false
            }
        }

        /// Meeting ended
        func onMeetingLeft() {
            // remove listeners
            meeting?.localParticipant.removeEventListener(self)
            meeting?.removeEventListener(self)
        }

        /// A new participant joined
        func onParticipantJoined(_ participant: Participant) {
            participants.append(participant)

            // add listener
            participant.addEventListener(self)

            participant.setQuality(.high)

            if(participant.isLocal){
                self.localParticipantViewContainer.isHidden = false
            } else {
                self.remoteParticipantViewContainer.isHidden = false
            }
        }

        /// A participant left from the meeting
        /// - Parameter participant: participant object
        func onParticipantLeft(_ participant: Participant) {
            participant.removeEventListener(self)
            guard let index = self.participants.firstIndex(where: { $0.id == participant.id }) else {
                return
            }
            // remove participant from list
            participants.remove(at: index)
            // hide from ui
            UIView.animate(withDuration: 0.5){
                if(!participant.isLocal){
                    self.remoteParticipantViewContainer.isHidden = true
                }
            }
        }

        /// Called when speaker is changed
        /// - Parameter participantId: participant id of the speaker, nil when no one is speaking.
        func onSpeakerChanged(participantId: String?) {

            // show indication for active speaker
            if let participant = participants.first(where: { $0.id == participantId }) {
                self.showActiveSpeakerIndicator(participant.isLocal ? localParticipantViewContainer : remoteParticipantViewContainer, true)
            }

            // hide indication for others participants
            let otherParticipants = participants.filter { $0.id != participantId }
            for participant in otherParticipants {
                if participants.count > 1 && participant.isLocal {
                    showActiveSpeakerIndicator(localParticipantViewContainer, false)
                } else {
                    showActiveSpeakerIndicator(remoteParticipantViewContainer, false)
                }
            }
        }

        func showActiveSpeakerIndicator(_ view: UIView, _ show: Bool) {
            view.layer.borderWidth = 4.0
            view.layer.borderColor = show ? UIColor.blue.cgColor : UIColor.clear.cgColor
        }

}

...

Step 6: Implementing ParticipantEventListener

In this stage, you'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 the MediaStream state.

MeetingViewController.swift

class MeetingViewController: UIViewController {

...

extension MeetingViewController: ParticipantEventListener {

        /// Participant has enabled mic, video or screenshare
        /// - Parameters:
        ///   - stream: enabled stream object
        ///   - participant: participant object
        func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
            updateUI(participant: participant, forStream: stream, enabled: true)
        }

        /// Participant has disabled mic, video or screenshare
        /// - Parameters:
        ///   - stream: disabled stream object
        ///   - participant: participant object
        func onStreamDisabled(_ stream: MediaStream, forParticipant participant: Participant) {
            updateUI(participant: participant, forStream: stream, enabled: false)
        }
}

private extension MeetingViewController {

    func updateUI(participant: Participant, forStream stream: MediaStream, enabled: Bool) { // true
        switch stream.kind {
        case .state(value: .video):
            if let videotrack = stream.track as? RTCVideoTrack {
                if enabled {
                    DispatchQueue.main.async {
                        UIView.animate(withDuration: 0.5){
                            if(participant.isLocal){
                                self.localParticipantViewContainer.isHidden =   false
                                self.localParticipantVideoView.isHidden = false
                                self.localParticipantVideoView.videoContentMode = .scaleAspectFill
                                self.localParticipantViewContainer.bringSubviewToFront(self.localParticipantVideoView)
                                videotrack.add(self.localParticipantVideoView)
                                self.lblLocalParticipantNoMedia.isHidden = true
                            } else {
                                self.remoteParticipantViewContainer.isHidden = false
                                self.remoteParticipantVideoView.isHidden = false
                                self.remoteParticipantVideoView.videoContentMode = .scaleAspectFill
                                self.remoteParticipantViewContainer.bringSubviewToFront(self.remoteParticipantVideoView)
                                videotrack.add(self.remoteParticipantVideoView)
                                self.lblRemoteParticipantNoMedia.isHidden = true
                            }
                        }
                    }
                } else {
                    UIView.animate(withDuration: 0.5){
                        if(participant.isLocal){
                            self.localParticipantViewContainer.isHidden = false
                            self.localParticipantVideoView.isHidden = true
                            self.lblLocalParticipantNoMedia.isHidden = false
                            videotrack.remove(self.localParticipantVideoView)
                        } else {
                            self.remoteParticipantViewContainer.isHidden = false
                            self.remoteParticipantVideoView.isHidden = true
                            self.lblRemoteParticipantNoMedia.isHidden = false
                            videotrack.remove(self.remoteParticipantVideoView)
                        }
                    }
                }
            }

        case .state(value: .audio):
            if participant.isLocal {
                localParticipantViewContainer.layer.borderWidth = 4.0
                localParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
            } else {
                remoteParticipantViewContainer.layer.borderWidth = 4.0
                remoteParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
            }
        default:
            break
        }
    }
}

...

Output

Video SDK Image

Known Issue

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

Video SDK Image

MeetingViewController.swift

override func viewDidLoad() {

    localParticipantVideoView.frame = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)

    localParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)

    localParticipantVideoView.clipsToBounds = true

    remoteParticipantVideoView.frame = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
    remoteParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
    remoteParticipantVideoView.clipsToBounds = true
}

Conclusion

  • By following this tutorial, You created a video calling app using the iOS platform, Xcode, and Swift.
  • Integrated the VideoSDK library with CocoaPods for seamless video calling functionality.
  • Implemented the Join Screen, allowing users to create new meetings or join existing ones.
  • Developed the Meeting Screen, which includes local and remote participant views and essential meeting controls like enabling/disabling the microphone and camera, and leaving the meeting.
  • Gained knowledge and hands-on experience in iOS app development, including integrating external libraries and handling meeting interactions.
  • If you are facing any issues, Feel free to join our Discord community. We would be happy to help.
Your Page Title

Schedule a Demo with Our Live Video Expert!

Discover how VideoSDK can help you build a cutting-edge real-time video app.

More IOS Resources

1