Skip to main content
Version: 4.x.x

Quick Start for Conference in iOS using SwiftUI

VideoSDK lets you add video & audio calling to Web, Android and iOS apps, with client SDKs and REST APIs.

This guide will get you running with the VideoSDK video & audio calling in minutes.

Prerequisites​

  • Xcode, and an iPhone or iPad to run the app on.

App Architecture​

This App will contain two screens:

  1. Join Screen : This screen lets the user either create a room or join a predefined room.

  2. Room Screen : This screen contains the local and remote participant views and some room controls such as Enable / Disable Mic & Camera, Screen Sharing and Leave Room.

Getting Started With the Code!​

Step 1: Create App​

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

Step 2: Choose App then click Next

Step 3: Add Product Name and Save the project.

Step 2: VideoSDK Installation​

VideoSDK 4.x is installed with Swift Package Manager (SPM). There is no 4.x CocoaPods release.

  1. Open your Xcode project and go to File > Add Package Dependencies….
  2. Enter the repository URL:
https://github.com/videosdk-live/videosdk-rtc-ios-spm
  1. Set Dependency Rule to Exact Version 4.0.0, and add the VideoSDKRTCSwift product to your app target.
  2. Import the library in Swift files:
import VideoSDKRTC

For more details, refer to the official guide on SPM installation.

Then declare the permissions in Info.plist:

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

Step 3: Project Structure​

iOSQuickStartDemo
├── iOSQuickStartDemo.swift // Default
├── Screens
├── JoinScreenView
└── JoinScreenView.swift
├── RoomView
└── RoomView.swift
└── RoomViewController.swift
├── RoomStruct.swift
└── Info.plist // Default

Step 4: Create Models​

Create a Swift file for the RoomStruct model.

RoomStruct.swift
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
}
}

struct Links: Codable {
let getRoom, getSession: String?

enum CodingKeys: String, CodingKey {
case getRoom = "get_room"
case getSession = "get_session"
}
}

Step 5: Create JoinScreen View​

We will create a joining screen that will act as our main screen. Create a new SwiftUI file and paste the following code.

JoinScreenView.swift
import SwiftUI

struct JoinScreenView: View {

// State variables for
@State var roomId: String
@State var name: String

var body: some View {

NavigationView {

VStack {

Text("VideoSDK")
.font(.largeTitle)
.fontWeight(.bold)
Text("Swift UI Quickstart")
.font(.largeTitle)
.fontWeight(.semibold)
.padding(.bottom)

TextField("Enter Room ID", text: $roomId)
.foregroundColor(Color.black)
.autocorrectionDisabled()
.font(.headline)
.overlay(
Image(systemName: "xmark.circle.fill")
.padding()
.offset(x: 10)
.foregroundColor(Color.gray)
.opacity(roomId.isEmpty ? 0.0 : 1.0)
.onTapGesture {
UIApplication.shared.endEditing()
roomId = ""
}
, alignment: .trailing)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.secondary.opacity(0.5))
.shadow(color: Color.gray.opacity(0.10), radius: 10))
.padding(.leading)
.padding(.trailing)

Text("Enter Room ID to join an existing room")

TextField("Enter Your Name", text: $name)
.foregroundColor(Color.black)
.autocorrectionDisabled()
.font(.headline)
.overlay(
Image(systemName: "xmark.circle.fill")
.padding()
.offset(x: 10)
.foregroundColor(Color.gray)
.opacity(name.isEmpty ? 0.0 : 1.0)
.onTapGesture {
UIApplication.shared.endEditing()
name = ""
}
, alignment: .trailing)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.secondary.opacity(0.5))
.shadow(color: Color.gray.opacity(0.10), radius: 10))
.padding()

if roomId.isEmpty == false {
NavigationLink(destination:{
if roomId.isEmpty == false {
RoomView(roomId: self.roomId, userName: name.isEmpty ? "Guest" : name)
.navigationBarBackButtonHidden(true)
} else {
}
}) {
Text("Join Room")
.foregroundColor(Color.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 25.0)
.fill(Color.blue)) }
}

NavigationLink(destination: RoomView(userName: name.isEmpty ? "Guest" : name)
.navigationBarBackButtonHidden(true)) {
Text("Start Room")
.foregroundColor(Color.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 25.0)
.fill(Color.blue)) }
}
}
}
}

// preview to show screen into your canvas
#Preview {
JoinScreenView(roomId: "", name: "")
}

extension UIApplication {

func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

Also add the JoinScreenView in main app as shown below

iOSQuickStartDemo.swift
import SwiftUI

@main
struct iOSQuickStartDemo: App {
var body: some Scene {
WindowGroup {
JoinScreenView(roomId: "", name: "")
}
}
}

Step 6: Create Room View​

Create a view for the room screen.

RoomView will implement the main room screen and the control buttons for the local participant.

The buttons call muteMic() / unmuteMic(), disableCam() / enableCam(), enableScreenShare() / disableScreenShare() and end(). These are async throws, so each button calls them with try await inside a Task. The mic and camera buttons read room.isMicEnabled and room.isCamEnabled to pick which call to make.

RoomView.swift
import SwiftUI
import VideoSDKRTC

struct RoomView: View{

@Environment(\.presentationMode) var presentationMode

// instance of RoomViewController which we will implement in next step
@StateObject var roomViewController = RoomViewController()
// Variables for keeping the state of various controls
@State var roomId: String?
@State var userName: String?

var body: some View {

VStack {
if roomViewController.participants.count == 0 {
Text("Room Initializing")
} else {
VStack {
VStack(spacing: 20) {
Text("Room ID: \(roomViewController.roomID)")
.padding(.vertical)

List {
ForEach(roomViewController.participants.indices, id: \.self) { index in
Text("Participant Name: \(roomViewController.participants[index].displayName)")
ZStack {
ParticipantView(track: roomViewController.participants[index].streams.first(where: { $1.kind == .video })?.value.track as? RTCVideoTrack).frame(height: 250)
if roomViewController.participants[index].streams.first(where: { $1.kind == .video }) == nil {
Color.white.opacity(1.0).frame(width: UIScreen.main.bounds.width, height: 250)
Text("No media")
}
}
}
}
}

VStack {
HStack(spacing: 15) {
// mic button
Button {
Task {
do {
// mutes the mic if it is on, unmutes it if it is off
guard let room = roomViewController.room else { return }
if room.isMicEnabled {
try await room.muteMic()
} else {
try await room.unmuteMic()
}
} catch let error as VideoSDKError where error == .ERROR_OPERATION_IN_PROGRESS {
// an earlier tap is still running
} catch {
print("Mic toggle failed: \(error)")
}
}
} label: {
Text("Toggle Mic")
.foregroundStyle(Color.white)
.font(.caption)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue))
}
// camera button
Button {
Task {
do {
// turns the camera off if it is on, on if it is off
guard let room = roomViewController.room else { return }
if room.isCamEnabled {
try await room.disableCam()
} else {
try await room.enableCam()
}
} catch let error as VideoSDKError where error == .ERROR_OPERATION_IN_PROGRESS {
// an earlier tap is still running
} catch {
print("Camera toggle failed: \(error)")
}
}
} label: {
Text("Toggle Camera")
.foregroundStyle(Color.white)
.font(.caption)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue))
}
}
HStack{
// screenshare button (OPTIONAL)
Button {
Task {
do {
// the share can also end from Control Center, so ask the SDK whether it is on
if roomViewController.room?.isScreenSharing == true {
try await roomViewController.room?.disableScreenShare()
} else {
try await roomViewController.room?.enableScreenShare()
}
} catch let error as VideoSDKError where error == .ERROR_GET_DISPLAY_MEDIA {
// the app is not set up for screen sharing yet (see Step 8)
print("Screen share is not set up: \(error.message)")
} catch let error as VideoSDKError
where error == .ERROR_ALREADY_IN_REQUESTED_STATE || error == .ERROR_OPERATION_IN_PROGRESS {
// already in that state, or the previous broadcast is still stopping
} catch {
print("Screen share failed: \(error)")
}
}
} label: {
Text("Toggle ScreenShare")
.foregroundStyle(Color.white)
.font(.caption)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue))
}

// end room button
Button {
Task {
do {
try await roomViewController.room?.end()
} catch {
// end() failed and you are still in the room, so leave it
print("End failed: \(error)")
try? await roomViewController.room?.leave()
}
presentationMode.wrappedValue.dismiss()
}
} label: {
Text("End Call")
.foregroundStyle(Color.white)
.font(.caption)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.red))
}
}
.padding(.bottom)
}
}
}
}.onAppear() {
if roomId?.isEmpty == false {
// join an existing room with provided room ID
roomViewController.joinRoom(roomId: roomId!, userName: userName!)
}
else {
// create a new room
roomViewController.joinRoom(userName: userName!)
}
}
}
}

/// VideoView for participant's video
class VideoView: UIView {

var videoView: RTCMTLVideoView = {
let view = RTCMTLVideoView()
view.videoContentMode = .scaleAspectFill
view.backgroundColor = UIColor.black
view.clipsToBounds = true
view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 250)

return view
}()

init(track: RTCVideoTrack?) {
super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 250))
backgroundColor = .clear
DispatchQueue.main.async {
self.addSubview(self.videoView)
self.bringSubviewToFront(self.videoView)
track?.add(self.videoView)
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

/// ParticipantView for showing and hiding VideoView
struct ParticipantView: UIViewRepresentable {
var track: RTCVideoTrack?

func makeUIView(context: Context) -> VideoView {
let view = VideoView(track: track)
view.frame = CGRect(x: 0, y: 0, width: 250, height: 250)
return view
}

func updateUIView(_ uiView: VideoView, context: Context) {
print("ui updated")
if track != nil {
track?.add(uiView.videoView)
} else {
track?.remove(uiView.videoView)
}
}
}

Step 7: Implement RoomViewController​

Here, we will write the code for initialising the room and controls for the same. You need an Auth token. Generate it with videosdk-server-api-example or from the Video SDK Dashboard.

RoomViewController will implement various event listeners such as RoomEventListener, ParticipantEventListener.

VideoSDK.createRoom(roomId:token:...) creates the Room and takes the token as a parameter. It throws ERROR_INVALID_PARAMETER for an empty room ID or token, so call it with try. join() is async throws, so call it with try await inside a Task. onError(error:) receives the errors that the SDK reports on its own, such as a camera or microphone that could not be opened.

RoomViewController.swift
import Foundation
import VideoSDKRTC

class RoomViewController: ObservableObject {

var token = "Your_Token"
var roomId: String = ""
var name: String = ""

@Published var room: Room? = nil
@Published var localParticipantView: VideoView? = nil
@Published var videoTrack: RTCVideoTrack?
@Published var participants: [Participant] = []
@Published var roomID: String = ""

func initializeRoom(roomId: String, userName: String) {
do {
room = try VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: userName,
micEnabled: true,
camEnabled: true
)
} catch {
print("Setup failed: \(error)") // ERROR_INVALID_PARAMETER for an empty room ID or token
return
}

room?.addEventListener(self)
Task {
do {
try await room?.join()
} catch {
print("Join failed: \(error)")
}
}
}
}

extension RoomViewController: RoomEventListener {

func onRoomJoined() {

guard let localParticipant = self.room?.localParticipant else { return }

// add to list
participants.append(localParticipant)

// add event listener
localParticipant.addEventListener(self)
}

func onParticipantJoined(_ participant: Participant) {

participants.append(participant)

// add listener
participant.addEventListener(self)
}

func onParticipantLeft(_ participant: Participant, reason: LeaveReason) {
participants = participants.filter({ $0.id != participant.id })
}

func onRoomLeft(reason: LeaveReason) {
participants.removeAll()

room?.localParticipant.removeEventListener(self)
room?.removeEventListener(self)
}

func onRoomStateChanged(roomState: RoomState) {
switch roomState {

case .DISCONNECTED:
participants.removeAll()

default:
print("")
}
}

// errors the SDK reports on its own, such as a camera or mic that could not be opened
func onError(error: VideoSDKError) {
print("VideoSDK error: \(error.name) (\(error.code)): \(error.message)")
}
}

extension RoomViewController: ParticipantEventListener {
func onStreamEnabled(_ stream: RTCStream, forParticipant participant: Participant) {

if participant.isLocal {
if let track = stream.track as? RTCVideoTrack {
DispatchQueue.main.async {
self.videoTrack = track
}
}
} else {
if let track = stream.track as? RTCVideoTrack {
DispatchQueue.main.async {
self.videoTrack = track
}
}
}
}

func onStreamDisabled(_ stream: RTCStream, forParticipant participant: Participant) {

if participant.isLocal {
if let _ = stream.track as? RTCVideoTrack {
DispatchQueue.main.async {
self.videoTrack = nil
}
}
} else {
self.videoTrack = nil
}
}
}

extension RoomViewController {
// create a new room ID
func joinRoom(userName: String) {

let urlString = "https://api.videosdk.live/v2/rooms"
let session = URLSession.shared
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue(self.token, forHTTPHeaderField: "Authorization")

session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in

if let data = data, let utf8Text = String(data: data, encoding: .utf8)
{
print("UTF =>=>\(utf8Text)") // original server data as UTF8 string
do{
let dataArray = try JSONDecoder().decode(RoomsStruct.self,from: data)
DispatchQueue.main.async {
// roomID is nil when the API rejects the token
guard let roomID = dataArray.roomID else {
print("Could not create a room: \(utf8Text)")
return
}
self.roomID = roomID
self.joinRoom(roomId: roomID, userName: userName)
}
print(dataArray)
} catch {
print(error)
}
}
}
).resume()
}

// initialise a room with the given room ID (either new or existing)
func joinRoom(roomId: String, userName: String) {

if !token.isEmpty {
// use provided token for the room
self.roomID = roomId
self.initializeRoom(roomId: roomId, userName: userName)
}
else {
print("Auth token required")
}
}
}

Step 8: Adding Broadcast Extension​

To make your app capable of sharing screen, you will need to add a broadcast upload extension to your app. Head over to the ScreenShare guide and follow the given steps to add the screen sharing capability in your app.

Until Info.plist has the RTCScreenSharingExtension and RTCAppGroupIdentifier keys and the app is in that App Group, enableScreenShare() throws ERROR_GET_DISPLAY_MEDIA.

tip

Stuck anywhere? Check out this example code on GitHub. The example uses iOS SDK 3.x; the Migration Guide lists what changed in 4.0.0.

Got a Question? Ask us on discord