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-swift2.14.1,components-swift0.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 β
Swift Package Manager (recommended) β
In Xcode: File ⸠Add Package Dependencies⦠and enter:
https://github.com/radioBros/RTCstack.gitAdd the products you need β RTCstackKit (SDK) and, for the UI kit, RTCstackUI. Or in Package.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 β
pod 'RTCstackKit', '~> 1.0'The SwiftUI UI layer (RTCstackUI) is SPM-only for now.
Quick start β
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:
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().
let call = RTCstack.createCall(.init(
token: jwt,
url: wssURL,
tokenRefresher: { try await myBackend.freshToken() } // optional, calls YOUR backend
))Connection β
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 JWTMedia control β
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 β
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:
@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) β
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).
try AudioSessionManager.shared.configure() // set category/mode before connectOne 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:
- 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.
- CallKit owns audio activation β media starts only after CallKit activates the session.
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 UIRTCstack 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:
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 UI | The SDK ships sources, not an app |
| Bundle IDs + signing team | Standard app signing |
| App Group shared with the Broadcast Extension | ReplayKit screen share |
| APNs VoIP key + PushKit entitlement | Incoming calls from a killed app |
Token-minting backend (POST /v1/token) | Secrets never ship in the binary |
Info.plist β
<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 β
| Component | Description |
|---|---|
VideoConferenceView | Full drop-in conference (grid + control bar) |
VideoGridView | Participant video grid with layout switching |
ParticipantVideoView | Single participant tile |
ControlBarView | Mic / camera / screen / reactions / layout / leave |
ChatPanelView | Scrolling 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 β
- Android SDK β
- Token Flow β minting tokens on your backend

