Skip to content

iOS SDK (Swift) ​

RTCstackKit is the Swift SDK β€” a thin, idiomatic wrapper over LiveKit's client-sdk-swift, mirroring the shipped @rtcstack/sdk Call API. RTCstackUI adds a drop-in SwiftUI conference view built on components-swift.

  • Min target: iOS 15.0+ (iPadOS 15.0+), Swift 5.9+
  • Pinned deps: client-sdk-swift 2.14.1, components-swift 0.1.7

Preview β€” install from source

The package is not yet tagged on CocoaPods trunk. Install via Swift Package Manager from git today; a CocoaPods release follows. Device features (CallKit/VoIP, ReplayKit) need a real-device pass with your own signing + push credentials.

Install ​

In Xcode: File β–Έ Add Package Dependencies… and enter:

https://github.com/radioBros/RTCstack.git

Add the products you need β€” RTCstackKit (SDK) and, for the UI kit, RTCstackUI. Or in Package.swift:

swift
dependencies: [
  .package(url: "https://github.com/radioBros/RTCstack.git", from: "1.0.0"),
],
targets: [
  .target(name: "MyApp", dependencies: [
    .product(name: "RTCstackKit", package: "RTCstack"),
    .product(name: "RTCstackUI",  package: "RTCstack"),
  ]),
]

CocoaPods on publish ​

ruby
pod 'RTCstackKit', '~> 1.0'

The SwiftUI UI layer (RTCstackUI) is SPM-only for now.

Quick start ​

swift
import RTCstackKit

// token + url come from YOUR backend (POST /v1/token) β€” never embed API secrets.
let call = RTCstack.createCall(.init(token: jwt, url: wssURL))
try await call.connect()

try await call.setMicEnabled(true)
try await call.setCameraEnabled(true)

The drop-in SwiftUI view:

swift
import RTCstackUI

struct CallScreen: View {
    let call: Call
    var body: some View {
        VideoConferenceView(call: call, onLeave: { /* dismiss */ })
            .rtcstackTheme()
    }
}

VideoConferenceView renders the video grid + control bar and handles connecting / disconnected states. It observes the Call (an ObservableObject) directly.

The Call API ​

Create with RTCstack.createCall(_:); it does not connect until you call connect().

swift
let call = RTCstack.createCall(.init(
    token: jwt,
    url: wssURL,
    tokenRefresher: { try await myBackend.freshToken() }  // optional, calls YOUR backend
))

Connection ​

swift
try await call.connect()      // mints/refreshes token if near expiry, then connects
await call.disconnect()       // idempotent
call.connectionState          // .idle | .connecting | .connected | .reconnecting | .disconnected
call.tokenExpiresAt           // Date, decoded from the JWT

Media control ​

swift
try await call.toggleMic()
try await call.setMicEnabled(true)
try await call.toggleCamera()
try await call.setCameraEnabled(true)
try await call.startScreenShare()   // see ReplayKit below
try await call.stopScreenShare()

Messaging ​

swift
try await call.sendMessage("hi")
try await call.sendMessage("psst", to: ["alice"])   // direct message
try await call.sendReaction("πŸ‘")

LiveKit does not echo your own sent data back; RTCstackUI renders your outgoing messages/reactions locally.

Reactive state (SwiftUI) ​

Call is an ObservableObject β€” observe @Published properties directly:

swift
@ObservedObject var call: Call
// call.participants : [String: Participant]
// call.localParticipant : Participant?
// call.activeSpeakers : [Participant]
// call.messages : [Message]
// call.layout : .grid | .speaker | .spotlight
// call.pinnedParticipant : String?

Discrete events (Combine) ​

swift
call.events.sink { event in
    switch event {
    case .participantJoined(let p):        print(p.name, "joined")
    case .messageReceived(let m):          appendChat(m.fromName, m.text)
    case .transcriptReceived(let seg):     appendTranscript(seg.speaker, seg.text)
    case .screenShareStarted(let p):       showScreenTile(p)
    case .reconnecting(let attempt):       showBanner(attempt)
    case .error(let code, let message):    handle(code, message)
    default: break
    }
}.store(in: &cancellables)

The full CallEvent enum mirrors the web SDK's CallEventMap, dropping the web-only audioPlaybackBlocked (no autoplay policy on native) and adding callSuspended / callResumed (app background/foreground) and permissionDenied(DeviceKind).

Native call stack ​

These modules make RTCstack feel like a native telephony app. They are opt-in but batteries-included.

Audio session ​

AudioSessionManager configures AVAudioSession for a VoIP call (.playAndRecord / .videoChat, Bluetooth + speaker options) and surfaces interruptions (phone call, Siri) and route changes (headphones / Bluetooth).

swift
try AudioSessionManager.shared.configure()   // set category/mode before connect

One audio-session owner

When using CallKit, CallKit activates the session (via its audio callbacks) β€” call configure() but not setActive(_:). Without CallKit, call setActive(true) on connect. Pick one owner.

CallKit + VoIP push ​

For incoming calls from a killed/suspended app, use CallCoordinator β€” it wires CallKitAdapter ↔ VoIPPushManager ↔ AudioSessionManager with the two orderings that are easy to get wrong:

  1. Report-before-return β€” on an incoming VoIP push iOS requires you to report a call to CallKit before the push handler returns, or the app is killed. The coordinator reports synchronously, then mints the token and connects on answer.
  2. CallKit owns audio activation β€” media starts only after CallKit activates the session.
swift
let coordinator = CallCoordinator(
    localizedName: "MyApp",
    tokenProvider: { roomId, callerId in
        try await myBackend.token(roomId: roomId, callerId: callerId)  // (token, url)
    },
    pushParser: { payload in
        // map YOUR VoIP push payload β†’ IncomingPush(roomId:callerId:callerName:)
    },
    onVoIPTokenUpdated: { token in myBackend.registerVoIPToken(token) }
)

coordinator.startOutgoingCall(roomId: "room-1", callerId: "me", handle: "Alice")
// observe coordinator.activeCall to drive your call UI

RTCstack never stores device tokens β€” onVoIPTokenUpdated hands the PushKit token to your notification service.

Screen share (ReplayKit) ​

iOS screen capture runs in a separate process β€” a Broadcast Upload Extension β€” not the host app. The SDK ships a SampleHandler template (BroadcastExtension/) and BroadcastPickerView, a SwiftUI wrapper over RPSystemBroadcastPickerView:

swift
BroadcastPickerView(preferredExtensionBundleId: "com.yourapp.Broadcast")
    .frame(width: 60, height: 60)

Wiring requires a Broadcast Upload Extension target + an App Group shared between the app and the extension (a common failure point β€” see the in-repo BroadcastExtension/ template).

What your app must provide ​

Why
Xcode app target hosting Example/ or your own UIThe SDK ships sources, not an app
Bundle IDs + signing teamStandard app signing
App Group shared with the Broadcast ExtensionReplayKit screen share
APNs VoIP key + PushKit entitlementIncoming calls from a killed app
Token-minting backend (POST /v1/token)Secrets never ship in the binary

Info.plist ​

xml
<key>NSCameraUsageDescription</key>      <string>Camera for video calls</string>
<key>NSMicrophoneUsageDescription</key>  <string>Microphone for calls</string>
<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
  <string>voip</string>
</array>

UI components ​

ComponentDescription
VideoConferenceViewFull drop-in conference (grid + control bar)
VideoGridViewParticipant video grid with layout switching
ParticipantVideoViewSingle participant tile
ControlBarViewMic / camera / screen / reactions / layout / leave
ChatPanelViewScrolling chat with input

Apply the design tokens (light/dark parity with the web kit) with the .rtcstackTheme() view modifier. See Theming for the shared token set.

Next steps ​