On this page

Swift Chat SDK 1.0.0 migration guide

The 1.0.0 release of the Swift Chat SDK introduces a set of API improvements that make real-time subscriptions more discoverable, event payloads strongly typed, and membership management more expressive. The core change is the shift from generic streaming methods to entity-owned callbacks: instead of calling chat.listenForEvents() or using channel.connect(), you now attach closures directly to entity objects (channel.onMessageReceived, user.onMentioned, etc.) or use the new .stream property for AsyncStream-based alternatives.

This guide is for developers who use Swift Chat SDK 0.x.x in existing applications.

If your application uses 0.x.x, it will not compile against 1.0.0 without changes to join(), Message.reactions, and Channel.delete() / User.delete(). This guide summarizes the differences between the versions and shows how to migrate to Swift Chat SDK 1.0.0.

Most notable changes include:

  • join() signature change: The return type changes from a tuple (membership:, messagesStream:) to Membership directly. A messagesStream is no longer returned — call onMessageReceived() or use channel.stream.messages() separately to start message delivery. Two new optional parameters, status and type, are added.
  • Entity-first callbacks: All streaming methods gain entity-owned alternatives. connect()onMessageReceived(), getTyping()onTypingChanged(), streamPresence()onPresenceChanged(), streamUpdates()onUpdated() + onDeleted(). The old methods are deprecated but still compile.
  • .stream property: Each entity exposes a .stream property providing AsyncStream-based equivalents for every callback method. For example, channel.stream.messages() is the AsyncStream equivalent of channel.onMessageReceived.
  • Typed event payloads: Events that previously required manual EventContent parsing now deliver purpose-built types. onMentioned() delivers Mention, onInvited() delivers Invite, onRestrictionChanged() delivers Restriction, and onReadReceiptReceived() delivers ReadReceipt.
  • Message.reactions type change: Message.reactions changed from [String: [Action]] to [MessageReaction]. Each MessageReaction exposes value, count, isMine, and userIds.
  • sendText() simplified: A new sendText(text:params:) overload consolidates the previous multi-parameter signature into a SendTextParams struct. The old overload is deprecated.
  • createThread() consolidated: The no-argument createThread(completion:) and the multi-parameter createThread(text:meta:shouldStore:...) overloads are deprecated. Use the new createThread(text:params:completion:) instead, which accepts a SendTextParams struct and returns a CreateThreadResult containing both the new ThreadChannel and the updated parent Message.
  • Soft delete removed: The soft: parameter is removed from Channel.delete() and User.delete(). Both methods now always perform a hard delete.
  • New Membership.delete(): Memberships can now be deleted directly from the Membership object.
  • New membership convenience methods: channel.hasMember(), channel.getMember(), user.isMemberOf(), and user.getMembership() let you check and retrieve a specific membership without fetching the full list.
  • New read receipt capabilities: channel.fetchReadReceipts() returns a paginated snapshot of read positions. A new emitReadReceiptEvents configuration option controls which channel types emit read receipt signals.
  • New custom event methods on Channel: channel.emitCustomEvent() and channel.onCustomEvent() replace the chat-level emitEvent() / listenForEvents() pattern for custom events. chat.listenForEvents() is deprecated.

Differences between 0.x.x and 1.0.0

Feature/Method0.x.x1.0.0
join() return type
(membership: MembershipImpl, messagesStream: AsyncStream<MessageImpl>)
MembershipImpl
join() signature
join(custom:)
join(custom:status:type:)
Message delivery after join()
Via messagesStream in the return tuple
Requires explicit onMessageReceived() or channel.stream.messages() call
connect()
Primary method for message delivery
Deprecated — use channel.onMessageReceived(callback:)
getTyping()
Typing indicator AsyncStream
Deprecated — use channel.onTypingChanged(callback:)
streamPresence()
Presence AsyncStream
Deprecated — use channel.onPresenceChanged(callback:)
streamUpdates() on entity
Update AsyncStream (single entity)
Deprecated — use onUpdated(callback:)
Deletion events
Part of streamUpdates() (nil value)
Separate onDeleted(callback:) event
streamReadReceipts()
AsyncStream<[Timetoken: [String]]> (timetoken → user IDs)
Deprecated — use channel.onReadReceiptReceived(callback:) with typed ReadReceipt
streamMessageReports()
AsyncStream<EventWrapper<EventContent.Report>>
Deprecated — use channel.onMessageReported(callback:) with typed Report
Mention events
chat.listenForEvents<EventContent.Mention>()
user.onMentioned(callback:) with typed Mention
Invite events
chat.listenForEvents<EventContent.Invite>()
user.onInvited(callback:) with typed Invite
Moderation events
chat.listenForEvents<EventContent.Moderation>()
user.onRestrictionChanged(callback:) with typed Restriction
Custom events (emit)
chat.emitEvent()
channel.emitCustomEvent()
Custom events (listen)
chat.listenForEvents()
channel.onCustomEvent(callback:)
listenForEvents()
Available
Deprecated
Message.reactions type
[String: [Action]]
[MessageReaction]
Message.actions
Available
Deprecated — use Message.reactions
sendText() signature
Multi-parameter (text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:)
sendText(text:params:) with SendTextParams; old overload deprecated
createThread()
createThread(completion:) and createThread(text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:completion:)
createThread(text:params:completion:) returning CreateThreadResult; old overloads deprecated
Channel.delete()
delete(soft: Bool = false)
delete() — no soft parameter; always hard deletes
User.delete()
delete(soft: Bool = false)
delete() — no soft parameter; always hard deletes
Membership.delete()
Not available
delete() available directly on Membership
Membership existence check
getMembers() then filter
channel.hasMember(userId:), channel.getMember(userId:)
User membership check
getMemberships() then filter
user.isMemberOf(channelId:), user.getMembership(channelId:)
Read receipts snapshot
Not available
channel.fetchReadReceipts()ReadReceiptsResponse
emitReadReceiptEvents config
Not available
Available in ChatConfiguration
.stream property on entities
Not available
AsyncStream-based equivalents for all callback methods

join() and onMessageReceived()

Important change

This change affects any code that calls join() and then reads from the returned messagesStream. Update it before releasing to users.

In 0.x.x, calling join() on a channel returned a tuple containing both the membership and an AsyncStream of messages — you could iterate the stream immediately to receive messages. In 1.0.0, join() creates the membership only and returns MembershipImpl directly. Call onMessageReceived(callback:) or iterate channel.stream.messages() separately to start receiving messages.

Two new optional parameters, status and type, let you set membership metadata at join time.

1// join() returned a tuple with both the membership and a message stream
2let result = try await channel.join(custom: ["role": "member"])
3let membership = result.membership
4
5// Messages arrived via the returned AsyncStream
6Task {
7 for await message in result.messagesStream {
8 print("New message: \(message.text ?? "")")
9 }
10}

Entity-first callbacks

In 0.x.x, streaming subscriptions were created by calling methods like connect(), getTyping(), or streamPresence() that returned an AsyncStream. You iterated the stream in a Task to receive events.

In 1.0.0, entity objects expose dedicated callback methods. Pass a closure directly to the method; it returns an AutoCloseable that you call .close() on to stop the subscription. For each callback method there is a matching .stream.*() method on the entity's .stream property that returns the AsyncStream equivalent.

The old methods (connect(), getTyping(), streamPresence(), streamUpdates()) are deprecated but still compile. Migrate to the new names at your own pace.

1// Channel — message delivery
2Task {
3 for await message in channel.connect() {
4 print("Message: \(message.text ?? "")")
5 }
6}
7
8// Channel — typing indicator
9Task {
10 for await typingUserIds in channel.getTyping() {
11 print("Typing: \(typingUserIds)")
12 }
13}
14
15// Channel — presence
show all 40 lines

Typed event payloads on User

Events that previously required calling chat.listenForEvents() with a generic EventContent type and parsing the payload manually now deliver purpose-built structs directly to entity-level callbacks.

Old methodNew methodNew payload type
chat.listenForEvents<EventContent.Mention>()
user.onMentioned(callback:)
Mention
chat.listenForEvents<EventContent.Invite>()
user.onInvited(callback:)
Invite
chat.listenForEvents<EventContent.Moderation>()
user.onRestrictionChanged(callback:)
Restriction
1// Mention events
2Task {
3 for await event in chat.listenForEvents(type: EventContent.Mention.self, channelId: user.id) {
4 if let payload = event.payload as? EventContent.Mention {
5 print("Mentioned in channel: \(payload.channel ?? "")")
6 }
7 }
8}
9
10// Invite events
11Task {
12 for await event in chat.listenForEvents(type: EventContent.Invite.self, channelId: user.id) {
13 if let payload = event.payload as? EventContent.Invite {
14 print("Invited to channel: \(payload.channel ?? "")")
15 }
show all 26 lines

Read receipts

Important change

This change affects any code that iterates streamReadReceipts(). Update the handler before releasing to users.

In 0.x.x, streamReadReceipts() yielded a [Timetoken: [String]] dictionary where keys were timetokens and values were lists of user IDs who had read up to that point. Aggregating across users required iterating the full dictionary on every event.

In 1.0.0:

  • onReadReceiptReceived(callback:) fires once per incoming receipt signal, delivering a single ReadReceipt value containing the user ID and their last-read timetoken. No manual aggregation is required.
  • channel.fetchReadReceipts() provides an on-demand snapshot of all current read receipts as [ReadReceipt].
  • A new emitReadReceiptEvents configuration property in ChatConfiguration controls which channel types emit read receipt signals.
1Task {
2 for await receipts in channel.streamReadReceipts() {
3 for (timetoken, userIds) in receipts {
4 print("Timetoken \(timetoken) read by \(userIds.count) user(s): \(userIds)")
5 }
6 }
7}

Controlling which channel types emit read receipts

A new emitReadReceiptEvents property in ChatConfiguration controls which channel types produce read receipt signals. The default setting disables read receipts on public channels, which is appropriate for most large-scale deployments.

1// Default: public = false, group = true, direct = true
2// To restore old behavior (always emit), set all to true:
3let config = ChatConfiguration(
4 emitReadReceiptEvents: [
5 .public: true,
6 .group: true,
7 .direct: true
8 ]
9)

Message reactions

In 0.x.x, message.reactions returned a [String: [Action]] dictionary. Grouping reactions by value, counting them, or checking whether the current user had reacted required manually iterating the dictionary and inspecting individual Action objects.

In 1.0.0, message.reactions returns [MessageReaction]. Each MessageReaction aggregates reactions of the same value and exposes:

  • value — the reaction string (for example, "👍")
  • count — derived from userIds.count
  • isMine — whether the current user is among the reactors
  • userIds — the full list of user IDs who added this reaction

The old message.actions property is deprecated.

1// Raw dictionary — manual grouping required
2for (reactionValue, actions) in message.reactions {
3 print("\(reactionValue): \(actions.count) reaction(s)")
4 // Checking if current user reacted required inspecting Action.uuid
5}

sendText()

In 0.x.x, sendText() accepted all message parameters as individual arguments. In 1.0.0, a new overload consolidates the optional parameters into a SendTextParams struct. The old overload is deprecated.

1try await channel.sendText(
2 text: "Hello everyone",
3 meta: ["importance": "high"],
4 shouldStore: true,
5 ttl: 24
6)

createThread()

Both createThread(completion:) (no parameters, returning only a ThreadChannel) and the multi-parameter createThread(text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:completion:) overload are deprecated. Use createThread(text:params:completion:) instead. The new method accepts a SendTextParams struct and returns a CreateThreadResult containing both the new ThreadChannel and the updated parent Message.

1// Create a thread and get only the ThreadChannel
2message.createThread { result in
3 switch result {
4 case .success(let threadChannel):
5 print("Thread created: \(threadChannel.id)")
6 case .failure(let error):
7 print("Error: \(error)")
8 }
9}

Custom events

chat.listenForEvents() is deprecated. Use channel.onCustomEvent() and channel.emitCustomEvent() instead.

1// Emit a custom event on a Channel
2try await channel.emitCustomEvent(
3 payload: ["chatID": "chat1234", "triggerWord": "frustrated"],
4 messageType: "customer-satisfaction",
5 storeInHistory: true
6)
7
8// Listen for custom events
9let stopCustom = channel.onCustomEvent(messageType: "customer-satisfaction") { event in
10 if let triggerWord = event.payload["triggerWord"]?.rawValue as? String {
11 print("Custom event — trigger: \(triggerWord)")
12 }
13}
14// AsyncStream: for await event in channel.stream.customEvents() { }
15stopCustom.close()

Soft delete removed from Channel and User

The soft: parameter has been removed from Channel.delete() and User.delete(). Both methods now always perform a hard (permanent) delete. If your application relied on soft deletion for any reason, migrate to hard deletion before upgrading.

A new Membership.delete() method is available to delete a membership directly from the Membership object.

1// delete() accepted a soft parameter
2try await channel.delete(soft: false)
3try await user.delete(soft: false)
4
5// Membership deletion required going through Chat or Channel
6try await chat.deleteChannel(id: channel.id)

New capabilities in 1.0.0

The following features are new in 1.0.0 and have no counterpart in 0.x.x.

FeatureDescription
fetchReadReceipts()
try await channel.fetchReadReceipts() returns a paginated snapshot of the current read position for all channel members as [ReadReceipt].
onReadReceiptReceived(callback:)
channel.onReadReceiptReceived { receipt in } delivers a typed ReadReceipt per incoming signal, replacing the raw [Timetoken: [String]] dictionary from streamReadReceipts().
onMessageReported(callback:)
channel.onMessageReported { report in } delivers a typed Report struct, replacing streamMessageReports().
.stream property
Every entity exposes a .stream property (for example, channel.stream, user.stream, membership.stream) providing AsyncStream-based equivalents for all callback methods.
hasMember(userId:) / getMember(userId:)
try await channel.hasMember(userId:) and try await channel.getMember(userId:) check and retrieve a specific member without fetching the full membership list.
isMemberOf(channelId:) / getMembership(channelId:)
try await user.isMemberOf(channelId:) and try await user.getMembership(channelId:) check and retrieve a specific membership from the User object.
Membership.delete()
try await membership.delete() removes a membership directly from the Membership object.
emitCustomEvent() / onCustomEvent()
channel.emitCustomEvent(payload:method:) and channel.onCustomEvent(callback:) provide a channel-scoped custom event API, replacing chat.emitEvent() / chat.listenForEvents().
createThread(text:params:)
Returns a CreateThreadResult containing both the ThreadChannel and the updated parent Message. The earlier no-argument createThread(completion:) and multi-parameter createThread(text:meta:shouldStore:...) overloads are deprecated.
getInvitees()
try await channel.getInvitees() returns the list of users who have been invited to the channel.
SendTextParams
New struct consolidating the optional parameters for sendText() and MessageDraft.send().
emitReadReceiptEvents config
ChatConfiguration.emitReadReceiptEvents ([ChannelType: Bool]) controls which channel types emit read receipt signals, letting you reduce unnecessary network traffic on public channels.
Membership.status and Membership.type
Two new properties on Membership that can be set when calling membership.update(status:type:).

Migration steps

To migrate from Swift Chat SDK 0.x.x to 1.0.0:

  1. Update every channel.join() call to handle the new return type. The method now returns MembershipImpl directly instead of a (membership:, messagesStream:) tuple. Remove any code that reads result.messagesStream and replace it with a call to channel.onMessageReceived(callback:) or iteration of channel.stream.messages().
  2. Replace channel.connect() with channel.onMessageReceived { message in }. Store the returned AutoCloseable and call .close() to stop message delivery.
  3. Replace channel.getTyping() with channel.onTypingChanged { typingUserIds in }. For AsyncStream, use channel.stream.typingChanges().
  4. Replace channel.streamPresence() with channel.onPresenceChanged { presentUserIds in }. For AsyncStream, use channel.stream.presenceChanges().
  5. Replace channel.streamUpdates() with separate channel.onUpdated { } and channel.onDeleted { } callbacks. The nil value that streamUpdates() emitted on deletion is replaced by the dedicated onDeleted event.
  6. Replace user.streamUpdates() with user.onUpdated { } and user.onDeleted { }.
  7. Replace membership.streamUpdates() with membership.onUpdated { } and membership.onDeleted { }.
  8. Replace channel.streamReadReceipts() with channel.onReadReceiptReceived { receipt in }. The payload changes from [Timetoken: [String]] to the ReadReceipt struct (userId, lastReadTimetoken). Remove any manual aggregation logic and replace full-snapshot reads with channel.fetchReadReceipts().
  9. Replace channel.streamMessageReports() with channel.onMessageReported { report in }. The payload changes from EventWrapper<EventContent.Report> to the Report struct.
  10. Replace chat.listenForEvents() for mention events with user.onMentioned { mention in }, for invite events with user.onInvited { invite in }, and for moderation events with user.onRestrictionChanged { restriction in }.
  11. Replace any remaining chat.listenForEvents() usage for custom events with channel.onCustomEvent { event in }, and replace chat.emitEvent() with channel.emitCustomEvent(payload:storeInHistory:). Read values from event.payload as [String: JSONCodable].
  12. Update all message.reactions access: the type changes from [String: [Action]] to [MessageReaction]. Replace raw actions property access — message.actions is deprecated.
  13. Replace channel.sendText(text:meta:shouldStore:ttl:...) calls with channel.sendText(text:params:) using the SendTextParams struct. Move meta, shouldStore, ttl, and other options into SendTextParams.
  14. Replace message.createThread(completion:) and the multi-parameter message.createThread(text:meta:shouldStore:...) with message.createThread(text:params:). Update callsites to use the CreateThreadResult return type to access both threadChannel and the updated message.
  15. Remove the soft: parameter from all channel.delete() and user.delete() calls. Both methods no longer accept this parameter and always perform a hard delete.
  16. Review ChatConfiguration initialization and add emitReadReceiptEvents if you need non-default read receipt emission behavior.

If you encounter issues during migration, contact PubNub Support.

Last updated on