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:)toMembershipdirectly. AmessagesStreamis no longer returned — callonMessageReceived()or usechannel.stream.messages()separately to start message delivery. Two new optional parameters,statusandtype, 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. .streamproperty: Each entity exposes a.streamproperty providingAsyncStream-based equivalents for every callback method. For example,channel.stream.messages()is theAsyncStreamequivalent ofchannel.onMessageReceived.- Typed event payloads: Events that previously required manual
EventContentparsing now deliver purpose-built types.onMentioned()deliversMention,onInvited()deliversInvite,onRestrictionChanged()deliversRestriction, andonReadReceiptReceived()deliversReadReceipt. Message.reactionstype change:Message.reactionschanged from[String: [Action]]to[MessageReaction]. EachMessageReactionexposesvalue,count,isMine, anduserIds.sendText()simplified: A newsendText(text:params:)overload consolidates the previous multi-parameter signature into aSendTextParamsstruct. The old overload is deprecated.createThread()consolidated: The no-argumentcreateThread(completion:)and the multi-parametercreateThread(text:meta:shouldStore:...)overloads are deprecated. Use the newcreateThread(text:params:completion:)instead, which accepts aSendTextParamsstruct and returns aCreateThreadResultcontaining both the newThreadChanneland the updated parentMessage.- Soft delete removed: The
soft:parameter is removed fromChannel.delete()andUser.delete(). Both methods now always perform a hard delete. - New
Membership.delete(): Memberships can now be deleted directly from theMembershipobject. - New membership convenience methods:
channel.hasMember(),channel.getMember(),user.isMemberOf(), anduser.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 newemitReadReceiptEventsconfiguration option controls which channel types emit read receipt signals. - New custom event methods on
Channel:channel.emitCustomEvent()andchannel.onCustomEvent()replace thechat-levelemitEvent()/listenForEvents()pattern for custom events.chat.listenForEvents()is deprecated.
Differences between 0.x.x and 1.0.0
| Feature/Method | 0.x.x | 1.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.
- 0.x.x
- 1.0.0
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}
1// join() creates the membership and returns it directly
2let membership = try await channel.join(
3 custom: ["role": "member"],
4 status: "active",
5 type: "standard"
6)
7
8// Subscribe to messages separately — closure callback
9let stopMessages = channel.onMessageReceived { message in
10 print("New message: \(message.text ?? "")")
11}
12
13// Or use the AsyncStream equivalent
14Task {
15 for await message in channel.stream.messages() {
show all 21 linesEntity-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.
- 0.x.x
- 1.0.0
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 lines1// Channel — message delivery (closure callback)
2let stopMessages = channel.onMessageReceived { message in
3 print("Message: \(message.text ?? "")")
4}
5// Or AsyncStream equivalent:
6// for await message in channel.stream.messages() { }
7
8// Channel — typing indicator
9let stopTyping = channel.onTypingChanged { typingUserIds in
10 print("Typing: \(typingUserIds)")
11}
12// Or: for await ids in channel.stream.typingChanges() { }
13
14// Channel — presence
15let stopPresence = channel.onPresenceChanged { presentUserIds in
show all 41 linesTyped 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 method | New method | New 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 |
- 0.x.x
- 1.0.0
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 lines1// Mention events — typed Mention struct
2let stopMentions = user.onMentioned { mention in
3 print("Mentioned in channel: \(mention.channelId)")
4 print("By user: \(mention.mentionedByUserId)")
5}
6// Or: for await mention in user.stream.mentions() { }
7
8// Invite events — typed Invite struct
9let stopInvites = user.onInvited { invite in
10 print("Invited to channel: \(invite.channelId)")
11 print("By user: \(invite.invitedByUserId)")
12}
13// Or: for await invite in user.stream.invites() { }
14
15// Moderation events — typed Restriction struct
show all 20 linesRead 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 singleReadReceiptvalue 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
emitReadReceiptEventsconfiguration property inChatConfigurationcontrols which channel types emit read receipt signals.
- 0.x.x
- 1.0.0
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}
1// Real-time updates — one callback per user receipt
2let stopReceipts = channel.onReadReceiptReceived { receipt in
3 print("User \(receipt.userId) read up to \(receipt.lastReadTimetoken)")
4}
5// Or AsyncStream equivalent:
6// for await receipt in channel.stream.readReceipts() { }
7
8// On-demand snapshot of all current receipts
9let response = try await channel.fetchReadReceipts()
10for receipt in response.receipts {
11 print("\(receipt.userId): \(receipt.lastReadTimetoken)")
12}
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 fromuserIds.countisMine— whether the current user is among the reactorsuserIds— the full list of user IDs who added this reaction
The old message.actions property is deprecated.
- 0.x.x
- 1.0.0
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}
1// Aggregated by reaction value — count and isMine included
2for reaction in message.reactions {
3 print("\(reaction.value): \(reaction.count) reaction(s), mine: \(reaction.isMine)")
4}
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.
- 0.x.x
- 1.0.0
1try await channel.sendText(
2 text: "Hello everyone",
3 meta: ["importance": "high"],
4 shouldStore: true,
5 ttl: 24
6)
1let params = SendTextParams(
2 meta: ["importance": "high"],
3 shouldStore: true,
4 ttl: 24
5)
6try await channel.sendText(text: "Hello everyone", params: params)
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.
- 0.x.x
- 1.0.0
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}
1// createThread() returns both the ThreadChannel and the updated parent Message
2let threadResult = try await message.createThread(
3 text: "Starting this thread",
4 params: SendTextParams(shouldStore: true)
5)
6let threadChannel = threadResult.threadChannel
7let updatedMessage = threadResult.message
8print("Thread channel ID: \(threadChannel.id)")
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.
- 0.x.x
- 1.0.0
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)
1// delete() is now parameter-free — always hard deletes
2try await channel.delete()
3try await user.delete()
4
5// Memberships can now be deleted directly
6try await membership.delete()
New capabilities in 1.0.0
The following features are new in 1.0.0 and have no counterpart in 0.x.x.
| Feature | Description |
|---|---|
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:
- Update every
channel.join()call to handle the new return type. The method now returnsMembershipImpldirectly instead of a(membership:, messagesStream:)tuple. Remove any code that readsresult.messagesStreamand replace it with a call tochannel.onMessageReceived(callback:)or iteration ofchannel.stream.messages(). - Replace
channel.connect()withchannel.onMessageReceived { message in }. Store the returnedAutoCloseableand call.close()to stop message delivery. - Replace
channel.getTyping()withchannel.onTypingChanged { typingUserIds in }. ForAsyncStream, usechannel.stream.typingChanges(). - Replace
channel.streamPresence()withchannel.onPresenceChanged { presentUserIds in }. ForAsyncStream, usechannel.stream.presenceChanges(). - Replace
channel.streamUpdates()with separatechannel.onUpdated { }andchannel.onDeleted { }callbacks. Thenilvalue thatstreamUpdates()emitted on deletion is replaced by the dedicatedonDeletedevent. - Replace
user.streamUpdates()withuser.onUpdated { }anduser.onDeleted { }. - Replace
membership.streamUpdates()withmembership.onUpdated { }andmembership.onDeleted { }. - Replace
channel.streamReadReceipts()withchannel.onReadReceiptReceived { receipt in }. The payload changes from[Timetoken: [String]]to theReadReceiptstruct (userId,lastReadTimetoken). Remove any manual aggregation logic and replace full-snapshot reads withchannel.fetchReadReceipts(). - Replace
channel.streamMessageReports()withchannel.onMessageReported { report in }. The payload changes fromEventWrapper<EventContent.Report>to theReportstruct. - Replace
chat.listenForEvents()for mention events withuser.onMentioned { mention in }, for invite events withuser.onInvited { invite in }, and for moderation events withuser.onRestrictionChanged { restriction in }. - Replace any remaining
chat.listenForEvents()usage for custom events withchannel.onCustomEvent { event in }, and replacechat.emitEvent()withchannel.emitCustomEvent(payload:storeInHistory:). Read values fromevent.payloadas[String: JSONCodable]. - Update all
message.reactionsaccess: the type changes from[String: [Action]]to[MessageReaction]. Replace rawactionsproperty access —message.actionsis deprecated. - Replace
channel.sendText(text:meta:shouldStore:ttl:...)calls withchannel.sendText(text:params:)using theSendTextParamsstruct. Movemeta,shouldStore,ttl, and other options intoSendTextParams. - Replace
message.createThread(completion:)and the multi-parametermessage.createThread(text:meta:shouldStore:...)withmessage.createThread(text:params:). Update callsites to use theCreateThreadResultreturn type to access boththreadChanneland the updatedmessage. - Remove the
soft:parameter from allchannel.delete()anduser.delete()calls. Both methods no longer accept this parameter and always perform a hard delete. - Review
ChatConfigurationinitialization and addemitReadReceiptEventsif you need non-default read receipt emission behavior.
If you encounter issues during migration, contact PubNub Support.