> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Voice Agent with iOS

> Connect a Zero Runtime voice AI agent to a native iOS (SwiftUI) app so users can talk to it in real time using the Google Gemini Live API.

Zero Runtime lets you integrate AI agents with real-time voice interaction into your native iOS app within minutes.

In this quickstart, you'll create an AI agent that joins a meeting room and interacts with users through voice using the Google Gemini Live API.

## Prerequisites

Before proceeding, ensure that your development environment meets the following requirements:

* A Zero Runtime account (create one from the [Zero Runtime Dashboard](https://app.zeroruntime.ai/)).
* iOS 13.0+, Xcode 13.0+, and Swift 5.0+.
* Python 3.11+ installed on your device.
* A Google API key with Gemini Live API access.

<Note>
  You need a Zero Runtime account to generate a token and a Google API key for the Gemini Live API.
  Generate a token from the [Zero Runtime Dashboard](https://app.zeroruntime.ai/) and a Google API key from [Google AI Studio](https://aistudio.google.com/api-keys).
</Note>

## Project Structure

Your final Xcode project structure should look like this:

<Tree>
  <Tree.Folder name="zrt-agents-quickstart-ios" defaultOpen>
    <Tree.File name="JoinScreenView.swift" />

    <Tree.File name="MeetingView.swift" />

    <Tree.File name="MeetingViewController.swift" />

    <Tree.File name="RoomsStruct.swift" />

    <Tree.File name="zrt_agents_quickstart_iosApp.swift" />
  </Tree.Folder>

  <Tree.Folder name="zrt-agents-quickstart-ios.xcodeproj" />

  <Tree.File name="agent-ios.py" />

  <Tree.File name=".env" />
</Tree>

You will be working on the following files:

* `JoinScreenView.swift`: Join screen UI.
* `MeetingView.swift`: Meeting interface with audio controls.
* `MeetingViewController.swift`: Handles meeting logic and events.
* `agent-ios.py`: The Python AI agent backend using the Google Gemini Live API.
* `.env`: Environment variables for the Python agent's API keys.

## 1. iOS Frontend

<Steps>
  <Step title="Step 1: Create the app and install the RTC SDK" icon="download">
    Create a new iOS app in Xcode:

    1. Create a new Xcode project.
    2. Choose the **App** template.
    3. Add a **Product Name** and save the project.

    Install the RTC SDK using Swift Package Manager:

    1. In Xcode, go to `File > Add Packages...`
    2. Enter the repository URL: `https://github.com/videosdk-live/videosdk-rtc-ios-sdk.git`
    3. Choose the latest version and click `Add Package`.

    Add microphone and camera permissions to `Info.plist`:

    ```xml title="Info.plist" theme={null}
    <key>NSCameraUsageDescription</key>
    <string>Camera permission description</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Microphone permission description</string>
    ```
  </Step>

  <Step title="Step 2: Create models and views" icon="window">
    Create the Swift models and views for the meeting interface.

    ```swift title="RoomsStruct.swift" theme={null}
    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"
        }
    }
    ```

    ```swift title="JoinScreenView.swift" theme={null}
    import SwiftUI

    struct JoinScreenView: View {
        
        // State variables for
        let meetingId: String = "YOUR_MEETING_ID"
        @State var name: String
        
        var body: some View {
            NavigationView {
                VStack {
                    Text("Zero Runtime")
                        .font(.largeTitle)
                        .fontWeight(.bold)
                    Text("AI Agent Quickstart")
                        .font(.largeTitle)
                        .fontWeight(.semibold)
                        .padding(.bottom)
                    
                    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()
                    
                    NavigationLink(destination: MeetingView(meetingId: self.meetingId, userName: name ?? "Guest")
                        .navigationBarBackButtonHidden(true)) {
                            Text("Join Meeting")
                                .foregroundColor(Color.white)
                                .padding()
                                .background(
                                    RoundedRectangle(cornerRadius: 25.0)
                                        .fill(Color.blue))
                        }
                }
            }
        }
    }

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

    ```swift title="MeetingView.swift" theme={null}
    import SwiftUI
    import VideoSDKRTC

    struct MeetingView: View{

    @Environment(\.presentationMode) var presentationMode

    @ObservedObject var meetingViewController = MeetingViewController()
    @State var meetingId: String?
    @State var userName: String?
    @State var isUnMute: Bool = true

    var body: some View {
        VStack {
            if meetingViewController.participants.count == 0 {
                Text("Meeting Initializing")
            } else {
                VStack {
                    VStack(spacing: 20) {
                        Text("Meeting ID: \(meetingViewController.meetingID)")
                            .padding(.vertical)
                        
                        List {
                            ForEach(meetingViewController.participants.indices, id: \.self) { index in
                                Text("Participant Name: \(meetingViewController.participants[index].displayName)")
                            }
                        }
                    }
                    
                    VStack {
                        HStack(spacing: 15) {
                            // mic button
                            Button {
                                if isUnMute {
                                    isUnMute = false
                                    meetingViewController.meeting?.muteMic()
                                }
                                else {
                                    isUnMute = true
                                    meetingViewController.meeting?.unmuteMic()
                                }
                            } label: {
                                Text("Toggle Mic")
                                    .foregroundStyle(Color.white)
                                    .font(.caption)
                                    .padding()
                                    .background(
                                        RoundedRectangle(cornerRadius: 25)
                                            .fill(Color.blue))
                            }
                            // end meeting button
                            Button {
                                meetingViewController.meeting?.end()
                                presentationMode.wrappedValue.dismiss()
                            } label: {
                                Text("End Call")
                                    .foregroundStyle(Color.white)
                                    .font(.caption)
                                    .padding()
                                    .background(
                                        RoundedRectangle(cornerRadius: 25)
                                            .fill(Color.red))
                            }
                        }
                        .padding(.bottom)
                    }
                }
            }
        }
        .onAppear() {
            /// MARK :- configuring the videoSDK
            VideoSDK.config(token: meetingViewController.token)
            print(meetingId)
            if meetingId?.isEmpty == false {
                print("i ff meeting isd is emty \(meetingId)")
                // join an existing meeting with provided meeting Id
                meetingViewController.joinMeeting(meetingId: meetingId!, userName: userName!)
            }
        }
    }
    }
    ```
  </Step>

  <Step title="Step 3: Implement meeting logic" icon="code">
    Create the main meeting view controller to handle meeting events.

    ```swift title="MeetingViewController.swift" theme={null}
    import Foundation
    import VideoSDKRTC

    class MeetingViewController: ObservableObject {
        
        var token = "YOUR_ZRT_AUTH_TOKEN" // Add Your token here
        var meetingId: String = ""
        var name: String = ""
        
        @Published var meeting: Meeting? = nil
        @Published var participants: [Participant] = []
        @Published var meetingID: String = ""
        
        func initializeMeeting(meetingId: String, userName: String) {
            meeting = VideoSDK.initMeeting(
                meetingId: meetingId,
                participantName: userName,
                micEnabled: true,
                webcamEnabled: false
            )
            
            meeting?.addEventListener(self)
            meeting?.join()
        }
    }

    extension MeetingViewController: MeetingEventListener {
        
        func onMeetingJoined() {
            guard let localParticipant = self.meeting?.localParticipant else { return }
            
            // add to list
            participants.append(localParticipant)
            
            localParticipant.addEventListener(self)
            
        }
        
        func onParticipantJoined(_ participant: Participant) {
            participants.append(participant)
            
            // add listener
            participant.addEventListener(self)
            
        }
        
        func onParticipantLeft(_ participant: Participant) {
            participants = participants.filter({ $0.id != participant.id })
        }
        
        func onMeetingLeft() {
            meeting?.localParticipant.removeEventListener(self)
            meeting?.removeEventListener(self)
        }
        
        func onMeetingStateChanged(meetingState: MeetingState) {
            switch meetingState {
            case .DISCONNECTED:
                participants.removeAll()
            default:
                print("")
            }
        }
    }

    extension MeetingViewController: ParticipantEventListener {
        
    }

    extension MeetingViewController {
            
        func joinMeeting(meetingId: String, userName: String) {
            if !token.isEmpty {
                self.meetingID = meetingId
                self.initializeMeeting(meetingId: meetingId, userName: userName)
            }
            else {
                print("Auth token required")
            }
        }
    }
    ```
  </Step>

  <Step title="Step 4: Configure the app entry point" icon="mobile">
    Configure the main app entry point.

    ```swift title="zrt_agents_quickstart_iosApp.swift" theme={null}
    import SwiftUI

    @main
    struct zrt_agents_quickstart_iosApp: App {
        var body: some Scene {
            WindowGroup {
                JoinScreenView(name: "")
            }
        }
    }
    ```
  </Step>

  <Step title="Step 5: Configure environment and credentials" icon="key">
    Create a meeting room using the Zero Runtime API:

    ```bash theme={null}
    curl -X POST https://api.videosdk.live/v2/rooms \
      -H "Authorization: YOUR_JWT_TOKEN_HERE" \
      -H "Content-Type: application/json"
    ```

    Copy the `roomId` from the response and set it as the `meetingId` in `JoinScreenView.swift`, and set your token in `MeetingViewController.swift` (`YOUR_ZRT_AUTH_TOKEN`).
  </Step>
</Steps>

## 2. Python AI Agent

<Steps>
  <Step title="Step 1: Setup the project" icon="download">
    Use Python 3.11 or higher, then install the Zero Runtime Agents SDK. Create and activate a virtual environment, then install `zrt` and `python-dotenv`. Every provider plugin ships with `zrt`.

    <CodeGroup>
      ```bash uv theme={null}
      uv venv --python 3.11
      source .venv/bin/activate     # macOS/Linux
      .\.venv\Scripts\activate      # Windows
      ```

      ```bash pip theme={null}
      python3.11 -m venv venv
      source venv/bin/activate      # macOS/Linux
      .\venv\Scripts\activate       # Windows
      ```
    </CodeGroup>

    <Tip>
      New to uv? See the [uv install guide](https://docs.astral.sh/uv/getting-started/installation/).
    </Tip>

    <CodeGroup>
      ```bash uv theme={null}
      uv pip install zrt python-dotenv
      ```

      ```bash pip theme={null}
      pip install zrt python-dotenv
      ```
    </CodeGroup>
  </Step>

  <Step title="Step 2: Configure environment variables" icon="key">
    Create a `.env` file in your project root to store your keys and token securely for the Python agent:

    ```shell title=".env" theme={null}
    # Google API Key for the Gemini Live API
    GOOGLE_API_KEY=your_google_api_key_here

    # Option A: pre-generated ZRT auth token
    ZRT_AUTH_TOKEN=your_zrt_auth_token_here

    # Option B: SDK auto-generates the token (omit ZRT_AUTH_TOKEN)
    # ZRT_API_KEY=your_zrt_api_key
    # ZRT_SECRET_KEY=your_zrt_secret_key
    ```
  </Step>

  <Step title="Step 3: Create the AI agent backend" icon="robot">
    Create the Python AI agent (`agent-ios.py`) that joins the same meeting room and interacts with users through voice.

    ```python title="agent-ios.py" theme={null}
    import os
    import zrt
    from zrt import Agent, Pipeline, Room
    from zrt.plugins import GeminiRealtime, GeminiLiveConfig
    from zrt.inference import AICousticsDenoise
    from dotenv import load_dotenv

    load_dotenv()

    AGENT_ID = "ios-quickstart-agent"

    pipeline = Pipeline(
        # Realtime speech-to-speech model goes in the pipeline's llm slot -
        # no separate STT or TTS needed.
        llm=GeminiRealtime(
            # When GOOGLE_API_KEY is set in .env you can omit api_key.
            api_key=os.getenv("GOOGLE_API_KEY"),
            config=GeminiLiveConfig(
                model="gemini-3.1-flash-live-preview",
                voice="Leda",  # Puck, Charon, Kore, Fenrir, Aoede, Leda, Orus, Zephyr
                response_modalities=["AUDIO"],
            ),
        ),
        denoise=AICousticsDenoise(model_id="quail-vf-2.2-l-16khz"),
    )


    class MyVoiceAgent(Agent):
        def __init__(self) -> None:
            super().__init__(
                name="MyVoiceAgent",
                agent_id=AGENT_ID,
                instructions="You are a high-energy game-show host guiding the caller to guess a secret number from 1 to 100 to win 1,000,000$.",
                pipeline=pipeline,
            )

        async def on_enter(self) -> None:
            await self.session.say(
                "Welcome to the Zero Runtime AI Agent game show! I'm your host, and we're about to play for 1,000,000$. Are you ready to play?"
            )

        async def on_exit(self) -> None:
            await self.session.say("Goodbye!")


    def invoke_agent() -> None:
        # Join the same room the frontend created (its meeting ID).
        zrt.invoke(AGENT_ID, room=Room(room_id="YOUR_MEETING_ID"))


    if __name__ == "__main__":
        zrt.serve(MyVoiceAgent, on_ready=invoke_agent)
    ```
  </Step>
</Steps>

## 3. Run the Application

<Steps>
  <Step title="Step 1: Run the iOS app" icon="play">
    Build and run the app from Xcode on a simulator or physical device.
  </Step>

  <Step title="Step 2: Run the AI agent" icon="terminal">
    Open a new terminal, activate your virtual environment, and run the Python agent. It reads `GOOGLE_API_KEY` from the `.env` file.

    <CodeGroup>
      ```bash uv theme={null}
      uv run python agent-ios.py
      ```

      ```bash pip theme={null}
      python agent-ios.py
      ```
    </CodeGroup>
  </Step>

  <Step title="Step 3: Connect and interact" icon="microphone">
    1. Run the iOS app on a simulator or device.
    2. Join the meeting and allow microphone permissions.
    3. When you join, the Python agent detects your participation and starts speaking.
    4. Talk to the agent in real time and play the number guessing game.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="'Waiting for participant...' but no connection" icon="plug">
    * Ensure the same `room_id` is set in both the iOS app (`JoinScreenView.swift`) and the agent's `Room(room_id=...)`.
    * Verify microphone permissions in iOS Settings > Privacy & Security > Microphone.
    * For simulator issues, ensure you're using a physical device for microphone testing.
  </Accordion>

  <Accordion title="Audio not working" icon="volume-high">
    * Check that microphone permissions were granted when prompted.
    * Ensure your Google API key has Gemini Live API access enabled.
  </Accordion>

  <Accordion title="Agent not responding" icon="robot">
    * Confirm your Zero Runtime token is valid and the `GOOGLE_API_KEY` is set in the `.env` file.
    * Check that the Gemini Live API is enabled in your Google Cloud Console.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Quickstart Example" icon="github" href="https://github.com/ZeroRuntimeAI/zrt-python-sdk-examples">
    Complete working example with source code.
  </Card>

  <Card title="Build Your First Voice Agent" icon="microphone" href="/quickstarts/build-your-first-voice-agent">
    Learn the Cascade and Realtime pipelines in depth.
  </Card>
</CardGroup>
