Skip to main content
Version: 3.x.x

Migration Guide

This guide covers what you need to change when upgrading the iOS SDK from v2.x.x to v3.0.0. Most of the work is in PubSub — everything else is a small number of listener signature updates.

1. PubSub methods are async throws

subscribe() and unsubscribe() are now asynchronous and throw. Call them with try await from a Task, and handle failures with do / catch. publish() was already awaited in v2.x.x, but the errors it throws are now typed (see section 2).

Before

extension MeetingViewController: MeetingEventListener {
func onMeetingJoined() {
meeting?.pubsub.subscribe(topic: "CHAT", forListener: self)
}

func onMeetingLeft() {
meeting?.pubsub.unsubscribe(topic: "CHAT", forListener: self)
}
}

After

extension MeetingViewController: MeetingEventListener {
func onMeetingJoined() {
Task {
do {
try await meeting?.pubsub.subscribe(topic: "CHAT", forListener: self)
} catch {
print("Subscribe failed: \(error.localizedDescription)")
}
}
}

func onMeetingLeft() {
Task {
do {
try await meeting?.pubsub.unsubscribe(topic: "CHAT", forListener: self)
} catch {
print("Unsubscribe failed: \(error.localizedDescription)")
}
}
}
}

Key points:

  • Errors now surface at the call site instead of only through MeetingEventListener.onError(error:). The global listener still receives the equivalent VideoSDKError, so use it for centralized logging and catch for handling a specific call.
  • Because the listener callbacks are not async, wrap the calls in a Task.

2. Typed PubSub errors

PubSub failures are now the PubSubError enum, so you can switch over the exact case instead of matching on strings. Cases carrying a String hold the underlying reason returned by the server.

Before

do {
try await meeting.pubsub.publish(topic: "CHAT", message: text)
} catch {
print("Error while sending message: \(error.localizedDescription)")
}

After

do {
try await meeting.pubsub.publish(topic: "CHAT", message: text)
} catch let error as PubSubError {
switch error {
case .meetingNotJoined:
print("Join the meeting before publishing")
case .meetingReconnecting:
print("Reconnecting — try again shortly")
case .operationTimeout:
print("PubSub socket is disconnected")
case .publishFailed(let reason):
print("Publish failed: \(reason)")
default:
print(error.localizedDescription)
}
}

Key points:

  • PubSubError conforms to LocalizedError, so error.localizedDescription still works if you don't need per-case handling.
  • VideoSDKError gained a type property that returns the constant name — for example "PUBSUB_SUBSCRIBE_FAILED" — which is useful when correlating iOS logs with your other platforms.
  • Learn more: Errors thrown by each PubSub method

3. getMessagesForTopic(_:) has been removed

In v2.x.x, persisted messages were cached locally and read back on demand. In v3.0.0 there is no cache — history is delivered to onOldMessagesReceived(_:info:) as it streams in, in batches.

Before

let messages = meeting.pubsub.getMessagesForTopic("CHAT")
render(messages)

After

extension MeetingViewController: PubSubMessageListener {
func onOldMessagesReceived(_ messages: [PubSubMessage], info: PubSubHistoryInfo) {
append(messages)
if info.isLast {
// Every persisted message for this topic has now been delivered.
render()
}
}
}

Key points:

  • History arrives in multiple batches. Accumulate them and use info.isLast to know when the last one has been delivered.
  • Use the oldMessageLimit option (see section 5) to cap how much history you fetch, or pass 0 to skip history entirely.

4. New PubSub listener callbacks

PubSubMessageListener gained three callbacks alongside onMessageReceived(_:). All four have default implementations, so a v2.x.x listener that only implements onMessageReceived(_:) still compiles — you opt in to the rest as you need them.

extension MeetingViewController: PubSubMessageListener {
// Existing — fires once per real-time message.
func onMessageReceived(_ message: PubSubMessage) { }

// New — the same real-time messages, grouped into a batch.
func onBatchReceived(_ messages: [PubSubMessage]) { }

// New — persisted messages, streamed in batches.
func onOldMessagesReceived(_ messages: [PubSubMessage], info: PubSubHistoryInfo) { }

// New — real-time messages shed because the device couldn't keep up.
func onMessageDrop(_ info: PubSubMessageDropInfo) { }
}

Key points:

  • On a busy topic, prefer onBatchReceived(_:) over onMessageReceived(_:) — one UI update per batch instead of one per message. They deliver the same messages, so implement one or the other, not both.
  • onMessageDrop(_:) only fires when realtimeOverflow is .drop.

5. New subscribe() options

subscribe(topic:forListener:) accepts an optional options: parameter of type PubSubSubscribeOptions, used to control how much history you fetch and what happens when messages arrive faster than your app can drain them. The parameter has a default value, so existing calls keep working unchanged.

var options = PubSubSubscribeOptions()
options.oldMessageLimit = 50 // fetch the 50 most recent persisted messages
options.realtimeOverflow = .queue // .queue (default) or .drop
options.maxQueue = 100 // only valid with .queue

try await meeting?.pubsub.subscribe(
topic: "CHAT",
forListener: self,
options: options
)

Key points:

  • oldMessageLimitnil (default) fetches all history, 0 fetches none, N fetches the most recent N.
  • realtimeOverflow.queue buffers messages so you receive them once you catch up; .drop sheds them and reports the count through onMessageDrop(_:). Use .queue for chat where every message matters, .drop for high-frequency, low-value streams such as reactions or cursor positions.
  • maxQueue — the buffer ceiling under .queue. Combining it with .drop throws PubSubError.maxQueueNotAllowedWithDrop.
  • newMessageLimit — caps how many live messages you accept per delivery window. 0 (default) means unlimited.
  • Invalid options are rejected synchronously, before any network work, so a bad configuration fails immediately rather than at runtime.
  • Learn more: subscribe() options

6. One subscription per listener, per topic

A listener may hold at most one subscription to a given topic. Subscribing the same listener to the same topic twice now throws PubSubError.alreadySubscribed instead of silently registering a second subscription.

What you should do

  • Call unsubscribe() before re-subscribing the same listener — for example when a view controller is re-created.
  • If you need to handle the same topic in two places, create a second listener and subscribe it separately.

7. onMeetingJoined(switchMeetingId:)

MeetingEventListener now delivers the join event through onMeetingJoined(switchMeetingId:) when meeting joins after switching from the one meeting to another. The parameter is the ID of the meeting you switched from when the join was triggered by switchTo, and nil on a normal join.

Before

func onMeetingJoined() {
print("Joined")
}

After

// Invokes when the meeting joined normally.
func onMeetingJoined() {
print("Joined")
}

// Invokes when the join was triggered by `switchTo` method.
func onMeetingJoined(switchMeetingId: String?) {
if let switchMeetingId {
print("Joined via room switch → \(switchMeetingId)")
} else {
print("Joined")
}
}

8. onCodecChanged(info:)

The codec change callback now receives a single CodecChangeInfo object instead of five positional arguments.

After

func onCodecChanged(info: CodecChangeInfo) {
print(info.currentCodec, info.previousCodec, info.unsupportedCodec)
print(info.kind, info.causedBy)
}

CodecChangeInfo carries currentCodec, previousCodec, unsupportedCodec, kind, and causedBy — the same values the old arguments provided, in the same order.


9. Livestream layout configuration

startLivestream(outputs:) gained an optional config: parameter of type LivestreamConfig, controlling layout, theme, and recording. It has a default value of nil, which uses the server defaults, so existing calls need no changes.

let config = LivestreamConfig()
config.layout = ConfigLayout(type: .GRID)

meeting?.startLivestream(outputs: outputs, config: config)

Got a Question? Ask us on discord