In this quickstart, we will introduce you to Daily's Client SDK for iOS by walking through the creation of a simple video call application. By the end of this walkthrough, you'll have a starting point for writing Daily-powered video apps for iOS with Swift.

This walkthrough will implement the following features:

  • Creating and joining a public Daily room
  • Asking for required application permissions
  • Displaying videos from local and remote room participants
  • Managing local microphone and camera input
  • Leaving the call

You can find the full codebase of this iOS app on GitHub.

Prerequisites

Before you get started, make sure you have a functioning iOS development environment, including the latest version of Xcode. You will also need a free Daily account.

If you want to test your app with a physical iOS device, make sure you have iOS 13.0 or newer, have enabled your device to run your own applications, and have enabled developer mode on your device.

Create a Daily room

Begin by navigating to Daily's developer dashboard and creating a new Daily room.

Create a room in the Daily Dashboard

Give your room a name and click the "Create room" button. All other options can be left at their defaults.

Make note of your Daily room URL. We'll be using it in the code shortly.

Create a new iOS project

Open Xcode and create a new project. Select the App template for your new project from the iOS tab:

Create a new project using the iOS template

In the project details window, enter your project and organization name. Select "Storyboard" in the "Interface" dropdown and click "Next":

Create a new project using the iOS template

You'll be prompted to pick a directory for your project. Once done, click "Create" to finish the process.

Add a dependency on Daily's Client SDK for iOS

With your new project created, you can add the Daily's iOS SDK as a dependency. When building an application using Xcode and Swift, you can use the Swift Package Manager to help you manage application dependencies.

In the top left corner of your Xcode window, press the File tab and select the "Add Packages..." option.

Add Daily dependencies to Xcode

In the search box at the top right hand corner, paste in the GitHub URL of Daily's iOS SDK repository: https://github.com/daily-co/daily-client-ios.git

A daily-client-ios package will appear:

Find the daily-client-ios package

Select "Branch" from the Dependency Rule dropdown and set the branch to main. Then, click "Add Package".

Xcode will now verify and load the package, then display a "Choose Package Products" dialog. Ensure "Daily" is selected and click "Add Package" again:

Add Daily dependencies to Xcode

Daily recommends using the latest stable version of the package that is compatible with your project requirements and your Swift version. Always check the release notes of the package to ensure there are no breaking changes that could impact your project.

In the project details page (which is shown by default), set the project's minimum deployment version to the version of iOS installed on your phone, which must be at least iOS 13.0.

Set the project's minimum deployment to iOS 13.0

Set application permissions

The application will need to be granted permissions to access the internet, use the device camera, and record and play audio.

Click on the parent Xcode project name in the project navigation sidebar:

Open project settings

Click on the "Build Settings" tab and then the "All" sub-tab:

Open build settings

In the "Filter" input box on the right, type "Camera". Once the "Privacy - Camera Usage Description" row appears, double click on the second column and enter: "Camera is necessary for transmitting video in a call":

Add camera usage description

Next, do the same with "Privacy - Microphone Usage Description", setting the description to "Microphone is necessary for transmitting audio in a call".

To ensure that audio will continue uninterrupted when your app is sent to the background, you also need to add the BackgroundModes capability. To do so, right-click on the Info.plist file. Choose "Open As -> Source Code" from the context menu. Then, copy and paste the XML code fragment below into the outermost parts of the file's body (<dict>...</dict>): add the following key and value to the top-level <dict>:

<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>

To confirm your changes are in the right place, refer to the resulting Info.plist file on GitHub.

Define the user interface

After implementing permission handling, we can define the user interface for our application. The UI will support the following functionality:

  • Displaying participants' video
  • Presenting call controls for toggling the camera/microphone and leaving the meeting

Open ViewController.swift and declare the following UI elements within the ViewController class:

// ViewController.swift
class ViewController: UIViewController {
// Buttons
@IBOutlet weak var cameraInputButton: UIButton!
@IBOutlet weak var microphoneInputButton: UIButton!
@IBOutlet weak var leaveRoomButton: UIButton!
// Participant views
@IBOutlet weak var participantsStack: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
//…
}
}

Next, we're going to configure the application layout in the main.storyboard file of the project. For that, you'll need some familiarity with the Xcode Interface Builder. Alternatively, you can copy the storyboard source from our existing demo repository.

Configuring the application layout through Xcode Interface Builder

If you're not already familiar with the Xcode Interface Builder, we've compiled some relevant resources below:

Adding objects to a storyboard:

Layout constraints:

Connecting storyboard objects to code:

Open the main.storyboard file and configure the required UI elements. These will be:

  • A UIButton to toggle the camera
  • A UIButton to toggle the microphone
  • A UIButton to leave the call
  • A UIStackView for the participant stack

Next, use the Xcode Objects Library to set the objects for the ViewController and configure their layout constraints.

Configure layout constraints

Now, connect the @IBOutlets you just defined in ViewController to their elements in the Storyboard:

Connect Outlets

Once the UI is configured, we're ready to build and run the application for the first time. To do so, click "Product -> Run" in the top application menu.

Once loaded, the application will prompt you to grant the necessary permissions. When permissions are granted, it should display the basic application interface you created:

Application loaded in simulator with a black screen at the top and call controls at the bottom

You won't see any video just yet because we haven't set up the call client or joined a Daily room. We'll do that next.

Setting up the call client

Now that the basics of the application are in place, we can shift our focus to joining and displaying participant videos.

The CallClient is the main interface into your Daily video call. It should be instantiated and retained for at least the duration of the video call session.

First, open ViewController.swift and import Daily:

import Daily

Next, in the ViewController class, create a instance of the CallClient object, a VideoView for the local participant, and a dictionary of VideoView.

Finally, create a constant called roomURLString for your Daily room URL.

// ViewController.swift
class ViewController: UIViewController {
// The client used to manage the video call.
let callClient: CallClient = .init()
// The local participant video view.
private let localVideoView: VideoView = .init()
// A dictionary of remote participant video views.
private var videoViews: [ParticipantId: VideoView] = [:]
// The URL for the room to join.
private let roomURLString: String = "[YOUR_DAILY_ROOM_URL]"
// Buttons
//...
// Participant views
//...
override func viewDidLoad() {
super.viewDidLoad()
//...
}
}

Add CallClient event listeners

With a CallClient instance created, we can set up our application to handle relevant events that occur during a video call. These events are emitted in response to actions like meeting participants joining, leaving, or being updated. We'll handle these events by adding an extension to have ViewController adopt the CallClientDelegate protocol.

Add the following declaration in ViewController.swift, underneath the original ViewController class:

// ViewController.swift
// Event listener delegate
extension ViewController: CallClientDelegate {
//…
}

In the ViewController viewDidLoad() method, set the call client's delegate to self:

//ViewController.swift
class ViewController: UIViewController {
//...
override func viewDidLoad() {
super.viewDidLoad()
// Set callClient's delegate
self.callClient.delegate = self
}
//...
}

To reflect accurate camera and microphone states in the local user's call controls, we'll create a function called updateControls() in the ViewController class. This function will update the state of the camera and microphone buttons. We'll also call this.updateControls() from the viewDidLoad() override method:

// ViewController.swift
class ViewController: UIViewController {
//...
override func viewDidLoad() {
super.viewDidLoad()
// Set callClient's delegate
self.callClient.delegate = self
self.updateControls()
}
// Update the UI according to the input's (e.g camera off/on)
private func updateControls() {
// Set the image for the camera button.
cameraInputButton.setImage(
UIImage(systemName: callClient.inputs.camera.isEnabled ? "video.fill": "video.slash.fill"),
for: .normal
)
// Set the image for the mic button.
microphoneInputButton.setImage(
UIImage(systemName: callClient.inputs.microphone.isEnabled ? "mic.fill": "mic.slash.fill"),
for: .normal
)
}
//...
}

To make sure the controls are updated when input settings actually change, add a handler for the inputsUpdated event within the CallClientDelegate extension:

extension ViewController: CallClientDelegate {
//...Previously implemented handlers above...
func callClient(_ callClient: CallClient, inputsUpdated inputs: InputSettings) {
print("Inputs Updated")
// Handle UI updates
self.updateControls()
}
}

When the local particpant's view loads, we should add their video to the stack view in viewDidLoad(). Additionally, disable the idle timer to keep the device screen active while the app is running. The final viewDidLoad() method will now look as follows:

override func viewDidLoad() {
super.viewDidLoad()
// Set the call client's delegate.
self.callClient.delegate = self
// Add the local participant's video view to the stack view.
self.participantsStack.addArrangedSubview(self.localVideoView)
// Disable the idle timer, so the screen will remain active while the app is in use.
UIApplication.shared.isIdleTimerDisabled = true
// Handle UI updates.
self.updateControls()
}

When remote participants join the Daily room, the app needs to display their video tracks. To do this, we'll listen for the participantJoined event in the CallClientDelegate extension:

// ViewController.swift
// Event listener delegate
extension ViewController: CallClientDelegate {
// Handle a remote participant joining
func callClient(_ callClient: CallClient, participantJoined participant: Participant) {
print("Participant \(participant.id) joined the call.")
// Create a new view for this participant's video track.
let videoView = VideoView()
// Determine whether the video input is from the camera or screen.
let cameraTrack = participant.media?.camera.track
let screenTrack = participant.media?.screenVideo.track
let videoTrack = screenTrack ?? cameraTrack
// Set the track for this participant's video view.
videoView.track = videoTrack
// Add this participant's video view to the dictionary.
self.videoViews[participant.id] = videoView
// Add this participant's video view to the stack view.
self.participantsStack.addArrangedSubview(videoView)
}
}

Above, when a remote participant joins the room, a new VideoView instance is created to display their video. The video view's track property is set to the participant's video track. The new participant is then added to a dictionary of video views. Finally, UI updates are performed and the view is set to update its layout.

We should also handle update events for call participants, which will inform the app when a participant toggles their camera or microphone. To do so, we'll handle the participantUpdated event:

// ViewController.swift
// Event listener delegate
extension ViewController: CallClientDelegate {
//...
// Handle a participant updating (e.g., their tracks changing)
func callClient(_ callClient: CallClient, participantUpdated participant: Participant) {
print("Participant \(participant.id) updated.")
// Determine whether the video track is for a screen or camera.
let cameraTrack = participant.media?.camera.track
let screenTrack = participant.media?.screenVideo.track
let videoTrack = cameraTrack ?? screenTrack
if participant.info.isLocal {
// Update the track for the local participant's video view.
self.localVideoView.track = videoTrack
} else {
// Update the track for a remote participant's video view.
self.videoViews[participant.id]?.track = videoTrack
}
}
}

Above, using our previously created videoViews dictionary, we can find the video view of the remote participant being updated and modify it accordingly. If the participant being updated is local, we update their tracks using loclVideoView.

Finally, when a participant leaves the meeting we'll handle the participantLeft:withReason event:

// ViewController.swift
// Event listener delegate
extension ViewController: CallClientDelegate {
//...
// Handle a participant leaving
func callClient(_ callClient: CallClient, participantLeft participant: Participant, withReason reason: ParticipantLeftReason) {
print("Participant \(participant.id) left the room.")
// Remove remote participant's video view from the dictionary and stack view.
if let videoView = self.videoViews.removeValue(forKey: participant.id) {
self.participantsStack.removeArrangedSubview(videoView)
}
}
}

After a participant leaves the room, their VideoView instance in the video views dictionary is removed.

Joining and leaving a Daily video call

With the CallClient instantiated set up to handle relevant Daily events, we're ready to have our app join a Daily room.

Create a new enterRoom() function within the ViewController class. Within it, invoke the call client's join() function, passing in the URL of the Daily room that you created earlier.

// ViewController.swift
class ViewController: UIViewController {
//...
override func viewDidLoad() {
super.viewDidLoad()
//...
}
private func enterRoom() {
guard let roomURL = URL(string: roomURLString) else {
return
}
// This call is where you would add a meeting token to join a private room.
callClient.join(url: roomURL, token: nil) { result in
print(result)
// You can either handle the join event in a callback or in a delegate method.
}
}
//...
}

Call the new enterRoom() method from the view controller's viewDidAppear() method:

// ViewController.swift
class ViewController: UIViewController {
//...
override func viewDidLoad() {
super.viewDidLoad()
//...
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Automatically join the room specified by `roomURLString` when this view controller appears.
self.enterRoom()
}
//...
}

To let the user leave the meeting, connect @IBAction didTapLeaveRoom(_ sender: Any) to the leaveRoomButton. Then, invoke the the call client's leave() method:

@IBAction func didTapLeaveRoom(_ sender: Any) {
self.callClient.leave()
}

Run the application. If you are running the app on actual hardware, you should see yourself as the local participant.

If you are running the application in the simulator, you should see a blank video view. This is because the simulator doesn't have access to your camera.

To test the app with a remote participant, open the room URL in your browser on any device. Once you've joined via the browser, the new participant's video should appear in the app view on your iOS device or simulator.

Remote video call participants in iOS simulator

Toggling microphone and camera states

Now that we have participant video being displayed in the app, we'll move on to implementing call controls.

Connect the @IBAction didTapToggleMicrophone(_ sender: Any) to the microphoneInputButton. Use the call client's setInputEnabled() instance method to toggle the microphone input:

@IBAction func didTapToggleMicrophone(_ sender: Any) {
// Disable or Enable the microphone.
let isEnabled = self.callClient.inputs.microphone.isEnabled
self.callClient.setInputEnabled(.microphone, !isEnabled)
}

Connect the @IBAction didTapToggleCamera(_ sender: Any) to the cameraInputButton. Use the call client's setInputEnabled() instance method to toggle the camera input:

@IBAction func didTapToggleCamera(_ sender: Any) {
// Disable or enable your camera
let isEnabled = self.callClient.inputs.camera.isEnabled
self.callClient.setInputEnabled(.camera, !isEnabled)
}

Wrapping up

This quickstart walked you through the basics of using the Daily's Client SDK for iOS to build a simple video call app. You should now be familiar with:

  • How to create the app with appropriate dependencies and permissions
  • How to join a Daily room
  • How to display participant video in your Android app
  • How to toggle the local participant's microphone and camera
  • How to leave the Daily room

To delve further into our iOS SDK, please refer to our reference documentation or reach out to our support team