Talk to us
Talk to us
menu

How to Develop an iOS Voice Call App

How to Develop an iOS Voice Call App

Adding voice calls to your iOS app helps keep users engaged and prevents them from switching to other platforms. With modern real-time communication tools, developers can easily integrate this feature without needing advanced audio knowledge. This guide covers practical steps for building an iOS voice call app, including setting up audio, managing calls, and creating a clear and user-friendly interface.

How to Build a Voice Call App with iOS

Creating a voice calling app for iOS becomes much simpler when you use ZEGOCLOUD’s ready-made iOS voice SDK. Popular apps like WhatsApp and Discord rely on real-time voice technology to connect millions of users. With ZEGOCLOUD, you get the same powerful audio technology that handles all the complex networking and audio processing.

In this section, we will build a complete voice calling app that connects users through clear audio calls. The app will handle multiple users, manage call states properly, and provide a clean interface for making and receiving calls.

Prerequisites

Before starting, make sure you have these requirements ready:

  • A ZEGOCLOUD account (sign up at console.zegocloud.com)
  • Your AppID and AppSign from ZEGOCLOUD Admin Console.
  • Xcode 13.0 or later installed on your Mac.
  • An iOS device running iOS 9.0 or later for testing.
  • Basic knowledge of Swift and iOS development.

If you have these ready, let’s start building your voice calling app!

1. Create Your iOS Project

First, create a new iOS project in Xcode for your voice calling app:

  1. Open Xcode and select “File > New > Project” from the menu. In the dialog box, choose the iOS tab and select “App” from the Application section.
  1. Fill in your project details with a meaningful name and your organization identifier. This creates your App Bundle Identifier that iOS uses to identify your app.
  1. Choose a directory to save your project and click “Create” to set up the basic project structure.

2. Install ZEGOCLOUD SDK

Add the ZEGOCLOUD Express SDK to your project using Swift Package Manager:

  1. Open your project in Xcode and select “File > Add Packages…” from the menu bar. In the search box, enter this URL: https://github.com/zegolibrary/express-video-ios
  2. Choose the SDK version you want to integrate (use “Up to Next Major Version” for the latest stable release). Click “Add Package” to download and integrate the SDK into your project.

Xcode will automatically handle the installation and make the SDK available for use in your code.

3. Add Device Permissions

Voice call apps need permission to use the device microphone. You can add these permissions to your app:

  1. In Xcode, select your project target and click on the “Info” tab. Under “Custom iOS Target Properties,” click the “+” button to add new permissions.
  1. Add these two privacy descriptions: Privacy - Microphone Usage Description: “This app needs microphone access to make voice calls” Privacy - Camera Usage Description: “This app needs camera access for video features” (optional for voice-only)

These descriptions tell users why your app needs these permissions when they first open it.

4. Set Up Your App Configuration

Create a configuration file to store your ZEGOCLOUD credentials securely. Add a new Swift file called Config.swift:

struct AppConfig {
    // Get these values from ZEGOCLOUD Console
    static let appID: UInt32 = your_app_id
    static let appSign: String = "your_app_sign"

    // Generate user tokens for authentication
    static func generateUserID() -> String {
        return "user_\(Int.random(in: 1000...9999))"
    }
}

Replace your_app_id and your_app_sign with the actual values from your ZEGOCLOUD console. These credentials authenticate your app with ZEGOCLOUD’s servers.

5. Create the Voice Engine

Set up the core voice calling engine that handles all audio processing. Add this code to your main view controller:

import UIKit
import ZegoExpressEngine

class VoiceCallViewController: UIViewController {
    private var currentRoomID: String = ""
    private var currentUserID: String = ""
    private var isInCall: Bool = false
    private var isMicrophoneMuted: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()
        setupVoiceEngine()
        setupUI()
    }

    private func setupVoiceEngine() {
        let profile = ZegoEngineProfile()
        profile.appID = AppConfig.appID
        profile.appSign = AppConfig.appSign
        profile.scenario = .standardVoiceCall

        // Create engine with voice call settings
        ZegoExpressEngine.createEngine(with: profile, eventHandler: self)
    }

    private func destroyEngine() {
        ZegoExpressEngine.destroy(nil)
    }
}

This creates the voice engine with settings optimized for voice calls rather than video or broadcasting.

6. Handle Voice Call Events

Implement event handlers to manage call states and audio streams. Extend your view controller with the event handler protocol:

extension VoiceCallViewController: ZegoEventHandler {

    // Handle new audio streams from other users
    func onRoomStreamUpdate(_ updateType: ZegoUpdateType, 
                           streamList: [ZegoStream], 
                           extendedData: [AnyHashable : Any]?, 
                           roomID: String) {

        if updateType == .add {
            // Someone started talking - play their audio
            for stream in streamList {
                ZegoExpressEngine.shared().startPlayingStream(stream.streamID)
            }
        } else {
            // Someone stopped talking - stop their audio
            for stream in streamList {
                ZegoExpressEngine.shared().stopPlayingStream(stream.streamID)
            }
        }
    }

    // Handle room connection changes
    func onRoomStateUpdate(_ state: ZegoRoomState, 
                          errorCode: Int32, 
                          extendedData: [AnyHashable : Any]?, 
                          roomID: String) {

        if state == .connected {
            print("Connected to call successfully")
            updateCallUI(connected: true)
        } else if state == .disconnected {
            print("Call ended or connection lost")
            updateCallUI(connected: false)
        }
    }

    // Handle other users joining or leaving
    func onRoomUserUpdate(_ updateType: ZegoUpdateType, 
                         userList: [ZegoUser], 
                         roomID: String) {

        for user in userList {
            if updateType == .add {
                print("User \(user.userName) joined the call")
            } else {
                print("User \(user.userName) left the call")
            }
        }
    }
}

These event handlers automatically manage audio streams and update your app when call states change.

7. Implement Call Functions

Add the core functions for starting, joining, and ending voice calls:

extension VoiceCallViewController {

    // Start or join a voice call
    func startCall(roomID: String, userID: String) {
        self.currentRoomID = roomID
        self.currentUserID = userID

        let user = ZegoUser(userID: userID)
        let config = ZegoRoomConfig()
        config.isUserStatusNotify = true

        // Join the call room
        ZegoExpressEngine.shared().loginRoom(roomID, user: user, config: config) { [weak self] errorCode, extendedData in
            if errorCode == 0 {
                // Successfully joined - start transmitting voice
                self?.startPublishingAudio()
                self?.isInCall = true
            } else {
                print("Failed to join call: \(errorCode)")
                self?.showError("Could not connect to call")
            }
        }
    }

    // Start transmitting your voice to others
    private func startPublishingAudio() {
        let streamID = "voice_\(currentUserID)"

        // Disable camera for voice-only calls
        ZegoExpressEngine.shared().enableCamera(false)

        // Start sending your voice
        ZegoExpressEngine.shared().startPublishingStream(streamID)
    }

    // End the current call
    func endCall() {
        // Stop sending your voice
        ZegoExpressEngine.shared().stopPublishingStream()

        // Leave the call room
        ZegoExpressEngine.shared().logoutRoom(currentRoomID)

        isInCall = false
        updateCallUI(connected: false)
    }

    // Toggle microphone on/off
    func toggleMicrophone() -> Bool {
        isMicrophoneMuted.toggle()
        ZegoExpressEngine.shared().muteMicrophone(isMicrophoneMuted)
        return isMicrophoneMuted
    }
}

These functions handle the main call actions users need: joining calls, speaking, and controlling their microphone.

8. Create the User Interface

Build a simple interface with buttons for call actions. Add this UI setup code:

extension VoiceCallViewController {

    private func setupUI() {
        view.backgroundColor = .systemBackground

        // Create call control buttons
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 20
        stackView.translatesAutoresizingMaskIntoConstraints = false

        let joinButton = createButton(title: "Join Call", action: #selector(joinCallTapped))
        let muteButton = createButton(title: "Mute", action: #selector(muteTapped))
        let endButton = createButton(title: "End Call", action: #selector(endCallTapped))

        stackView.addArrangedSubview(joinButton)
        stackView.addArrangedSubview(muteButton)
        stackView.addArrangedSubview(endButton)

        view.addSubview(stackView)

        // Center the buttons
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.widthAnchor.constraint(equalToConstant: 200)
        ])
    }

    private func createButton(title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        button.addTarget(self, action: action, for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        return button
    }

    // Button action handlers
    @objc private func joinCallTapped() {
        let roomID = "demo_room_123"
        let userID = AppConfig.generateUserID()
        startCall(roomID: roomID, userID: userID)
    }

    @objc private func muteTapped(_ sender: UIButton) {
        let isMuted = toggleMicrophone()
        sender.setTitle(isMuted ? "Unmute" : "Mute", for: .normal)
    }

    @objc private func endCallTapped() {
        endCall()
    }

    private func updateCallUI(connected: Bool) {
        // Update button states and UI based on call status
        DispatchQueue.main.async {
            // Update your UI elements here
        }
    }

    private func showError(_ message: String) {
        DispatchQueue.main.async {
            let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(alert, animated: true)
        }
    }
}

With the code above, we created a clean interface with the essential call controls users need.

9. Handle App Lifecycle

Make sure the voice engine starts and stops properly with your app:

extension VoiceCallViewController {

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // Clean up when leaving the screen
        if isInCall {
            endCall()
        }
        destroyEngine()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // Restart engine if needed when returning to screen
        setupVoiceEngine()
    }
}

This ensures your app properly manages system resources and doesn’t leave calls running in the background.

Run a Demo

To test your implementation, you need at least two devices or one device and the web testing platform. Install your app on two different iOS devices. Have each device join the same room with different user IDs.

Conclusion

You now have a fully functional voice calling app that handles real-time audio communication between users! Using ZEGOCLOUD’s powerful SDK, you built professional-grade voice calling features without needing deep audio engineering knowledge. Your users can now make clear voice calls, control their microphone, and connect with others seamlessly.

This foundation opens up many possibilities for enhancing your app. Consider adding features like call history, contact lists, group voice calls, or integration with your existing user system. With the core voice technology working, you can focus on building unique features that make your app stand out in the market.

FAQ

Q1: How can I implement voice calls in an iOS app without interruptions?

Use CallKit for native call UI and PushKit for VoIP push notifications. These ensure the app can wake up and display a proper interface even if it’s in the background. Managing audio sessions correctly is also key to preventing call disruptions.

Q2: What frameworks are required to build voice calling on iOS?

You’ll need AVFoundation for handling audio, CallKit for the native calling interface, and PushKit for VoIP push notifications. For actual call functionality, integrating a real-time SDK like WebRTC or ZEGOCLOUD is recommended.

Q3: What common mistakes do beginners make when building iOS voice calls?

Common issues include missing microphone permission requests, incorrect audio session setup, and improper use of CallKit or VoIP pushes. These can lead to no sound, failed calls, or app rejections if not handled correctly.

Let’s Build APP Together

Start building with real-time video, voice & chat SDK for apps today!

Talk to us

Take your apps to the next level with our voice, video and chat APIs

Free Trial
  • 10,000 minutes for free
  • 4,000+ corporate clients
  • 3 Billion daily call minutes

Stay updated with us by signing up for our newsletter!

Don't miss out on important news and updates from ZEGOCLOUD!

* You may unsubscribe at any time using the unsubscribe link in the digest email. See our privacy policy for more information.