A SBDMessageCollection retrieves data from both the local cache and Sendbird server and lets you quickly create a chat view without missing any message-related updates. This page explains how to create a chat view using the collection and serves as a migration guide.
In local caching, the SBDMessageListParams is used instead of SyncManager's MessageFilter.
A SBDMessageListParams instance will determine how to sort and list the retrieved messages. Then, specify the starting point of the message list in the chat view using the collection's builder.
Use the SBDMessageCollectionDelegate to determine how the client app reacts to message-related events.
A new addition to local caching is the didDetectHugeGap:. If more than 300 messages are missing in the local cache compared to the remote server, Sendbird Chat SDK determines that there is a huge gap. For more information, see Gap and synchronization.
The following table shows when to call each event handler.
Delegate
Called when
messageCollection:context:channel:addedMessages:
- A new message is created as a real-time event. - New messages are fetched during changelog sync.
- A message is deleted as a real-time event. - Message deletion is detected during changelog sync. - The value of the SBDMessageListParams setter such as custom_type changes.
- A message is updated as a real-time event. - Message update is detected during changelog sync. - The sending status of a pending message changes.
messageCollection:context:updatedChannel:
- The channel information that is included in the user's current chat view is updated as a real-time event. -Channel info update is detected during changelog sync.
messageCollection:context:deletedChannel:
- The current channel is deleted as a rea-time event. - Channel deletion is detected during changelog sync. - In both cases, the entire view should be disposed of.
didDetectHugeGap:
- A huge gap is detected through Background sync. In this case, you need to dispose of the view and create a new SBDMessageCollection instance.
SyncManager's didReceive:succeededMessages:,didReceive:pendingMessages:, and didReceive:failedMessages:should be changed as shown in the code below.
extension ViewController: SBDMessageCollectionDelegate {
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:addedMessages: in local caching:
* 1. collection(_:didReceive:succeededMessages:) SBSMMessageEventAction.insert.
* 2. collection(_:didReceive:pendingMessages:) SBSMMessageEventAction.insert.
*/
func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, addedMessages messages: [SBDBaseMessage]) {
}
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:updatedMessages: in local caching:
* 1. collection(:didReceive:succeededMessages:) SBSMMessageEventAction.update.
* 2. collection(_:didReceive:pendingMessages:) SBSMMessageEventAction.insert when resending a failed message.
* 3. collection(_:didReceive:failedMessages:) SBSMMessageEventAction.insert when sending or resending a message failed.
*/
func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, updatedMessages messages: [SBDBaseMessage]) {
}
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:deletedMessages: in local caching:
* 1. collection(_:didReceive:succeededMessages:) SBSMMessageEventAction.remove.
* 2. collection(_:didReceive:failedMessages:) SBSMMessageEventAction.remove.
*/
func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, deletedMessages messages: [SBDBaseMessage]) {
}
/**
* SyncManager's (_:didUpdate:) events should be handled through the messageCollection:context:updatedChannel: in local caching.
*/
func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, updatedChannel channel: SBDGroupChannel) {
}
/**
* SyncManager's(_:didRemove:) events should be handled through the messageCollection:context:deletedChannel: in local caching.
*/
func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, deletedChannel channelUrl: String) {
}
/**
* The didDetectHugeGap() is called when the SDK detects more than 300 messages missing while connecting online.
* The current message collection should be disposed and a new MessageCollection is created.
*/
func didDetectHugeGap(_ collection: SBDMessageCollection) {
if messageCollection != nil {
messageCollection!.dispose()
}
let firstItemPosition = self.getFirstItemPosition()
if firstItemPosition >= 0 {
let message = self.messages[firstItemPosition]
messageCollection = SBDMessageCollection(channel: GROUP_CHANNEL, startingPoint: message.createdAt, params: messageListParams)
}
else {
messageCollection = SBDMessageCollection(channel: GROUP_CHANNEL, startingPoint: messageCollection!.startingPoint, params: messageListParams)
}
messageCollection?.delegate = self
}
}
// LocalCachingViewController.h
@interface LocalCachingViewController : UIViewController<SBDMessageCollectionDelegate>
@end
// LocalCachingViewController.m
@implementation LocalCachingViewController
#pragma mark - SBDMessageCollectionDelegate
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:addedMessages: in local caching:
* 1. collection(_:didReceive:succeededMessages:) SBSMMessageEventAction.insert.
* 2. collection(_:didReceive:pendingMessages:) SBSMMessageEventAction.insert.
*/
- (void)messageCollection:(SBDMessageCollection *)collection
context:(SBDMessageContext *)context
channel:(SBDGroupChannel *)channel
addedMessages:(NSArray<SBDBaseMessage *> *)messages {
}
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:updatedMessages: in local caching:
* 1. collection(:didReceive:succeededMessages:) SBSMMessageEventAction.update.
* 2. collection(_:didReceive:pendingMessages:) SBSMMessageEventAction.insert when resending a failed message.
* 3. collection(_:didReceive:failedMessages:) SBSMMessageEventAction.insert when sending or resending a message failed.
*/
- (void)messageCollection:(SBDMessageCollection *)collection
context:(SBDMessageContext *)context
channel:(SBDGroupChannel *)channel
updatedMessages:(NSArray<SBDBaseMessage *> *)messages {
}
/**
* SyncManager's following events should be handled through the messageCollection:context:channel:deletedMessages: in local caching:
* 1. collection(_:didReceive:succeededMessages:) SBSMMessageEventAction.remove.
* 2. collection(_:didReceive:failedMessages:) SBSMMessageEventAction.remove.
*/
- (void)messageCollection:(SBDMessageCollection *)collection
context:(SBDMessageContext *)context
channel:(SBDGroupChannel *)channel
deletedMessages:(NSArray<SBDBaseMessage *> *)messages {
}
/**
* SyncManager's (_:didUpdate:) events should be handled through the messageCollection:context:updatedChannel: in local caching.
*/
- (void)messageCollection:(SBDMessageCollection *)collection
context:(SBDMessageContext *)context
updatedChannel:(SBDGroupChannel *)channel {
}
/**
* SyncManager's(_:didRemove:) events should be handled through the messageCollection:context:deletedChannel: in local caching.
*/
- (void)messageCollection:(SBDMessageCollection *)collection
context:(SBDMessageContext *)context
deletedChannel:(NSString *)channelUrl {
}
/**
* The didDetectHugeGap() is called when the SDK detects more than 300 messages missing while connecting online.
* The current message collection should be disposed and a new MessageCollection is created.
*/
- (void)didDetectHugeGap:(SBDMessageCollection *)collection {
if (self.messageCollection != nil) {
[self.messageCollection dispose];
}
NSInteger firstItemPosition = [self getFirstItemPosition];
if (firstItemPosition >= 0) {
SBDBaseMessage *message = self.messages[firstItemPosition];
self.messageCollection = [[SBDMessageCollection alloc] initWithChannel:GROUP_CHANNEL startingPoint:message.createdAt params:messageListParams];
}
else {
self.messageCollection = [[SBDMessageCollection alloc] initWithChannel:GROUP_CHANNEL startingPoint:self.messageCollection.startingPoint params:messageListParams];
}
self.messageCollection.delegate = self;
}
@end
SyncManager's didReceiveNewMessage: which notifies the client app of new messages in a channel should also be changed as shown in the code below.
SyncManager's fetchInDirection method retrieves messages from the local cache and delivers them to the SBSMMessageCollectionDelegate. In local caching, the SBDMessageCollection can retrieve and display messages through four new interfaces, hasPrevious, hasNext, loadPrevious, and loadNext.
Unlike the SBDGroupChannelCollection, pagination works in both directions for messages because messages can be shown in either chronological or reverse chronological order depending on how you set the value of the startingPoint.
Method
Description
hasPrevious
- Checks if there are more messages to load from the previous page. - Called whenever a user scroll hits the top of the chat view.
loadPrevious
- If hasPrevious is true, retrieves messages from the local cache to show in the view. - Called whenever a user scroll hits the top of the chat view.
hasNext
- Checks if there are more messages to load in the next page. - Called whenever a user scroll hits the bottom of the chat view.
loadNext
- If hasNext is true, retrieves messages from the local cache to show in the view. - Called whenever a user scroll hits the bottom of the chat view.
In a SBDMessageCollection, the initialization is dictated by the SBDMessageCollectionInitPolicy. The SBDMessageCollectionInitPolicy determines how initialization deals with the message data retrieved from the local cache and API calls. Because we only support the cacheAndReplaceByApi at this time, you should clear all messages in the local cache before adding messages from the remote server. Messages will first be retrieved from the cached list using cacheResultHandler:. Next, the apiResultHandler: calls the API result list which then replaces the cached message list with messages received from the API call.
// Initialize messages from the startingPoint.
messageCollection?.start(with: .cacheAndReplaceByApi, cacheResultHandler: { messages, error in
// Messages will be retrieved from the local cache.
// They might be too outdated compared to the startingPoint.
}, apiResultHandler: { messages, error in
// Messages will be retrieved through API calls from Sendbird server.
// According to the .cacheAndReplaceByApi,
// the existing data source needs to be cleared
// before adding retrieved messages to the local cache.
})
// Next direction
if messageCollection?.hasNext {
messageCollection?.loadNext(completionHandler: { messages, error in
// A message list returns as a callback.
})
}
// Previous direction
if messageCollection?.hasPrevious {
messageCollection?.loadPrevious(completionHandler: { messages, error in
})
}
[self.messageCollection startCollectionWithInitPolicy:SBDMessageTypeFilterAll cacheResultHandler:^(NSArray<SBDBaseMessage *> * _Nullable messages, SBDError * _Nullable error) {
// Messages will be retrieved from the local cache.
// They might be too outdated compared to the startingPoint.
} apiResultHandler:^(NSArray<SBDBaseMessage *> * _Nullable messages, SBDError * _Nullable error) {
// Messages will be retrieved through API calls from Sendbird server.
// According to the .cacheAndReplaceByApi,
// the existing data source needs to be cleared
// before adding retrieved messages to the local cache.
}];
// Next direction
if (self.messageCollection.hasNext) {
[self.messageCollection loadNextWithCompletionHandler:^(NSArray<SBDBaseMessage *> * _Nullable messages, SBDError * _Nullable error) {
// A message list returns as a callback.
}];
}
// Previous direction
if (self.messageCollection.hasPrevious) {
[self.messageCollection loadPreviousWithCompletionHandler:^(NSArray<SBDBaseMessage *> * _Nullable messages, SBDError * _Nullable error) {
// A message list returns as a callback.
}];
}
A viewpoint is a timestamp option that sets the starting point of the current user's view. This viewpoint can be reset anytime by calling the resetViewpointTimestamp() method. SyncManager's resetViewpointTimestamp is used to reset the timestamp of the current message collection to another specified time. To reset a viewpoint timestamp in local caching, you should dispose of the current message collection and create a new one.
In local caching, the result of sending a message is handled internally through the SBDMessageCollectionDelegate. First, pending messages are delivered to local caching's messageCollection(_:context:channel:addedMessages:). Whether the message succeeds or fails in sending, the result will be delivered to the messageCollection(_:context:channel:updatedMessages:). Thus, in local caching, it is not necessary to manually send the callbacks from the sendUserMessage() and the sendFileMessage() to the handleSendMessageResponse() as in SyncManager.
Note: Don't add the pending, succeeded or failed message objects of the sendMessage() callback to your message list data source. This can cause duplicate messages in the chat view.
In local caching, the result of resending a message is handled internally through the SBSMMessageCollectionDelegate. First, pending messages are delivered to local caching's messageCollection(_:context:channel:updatedMessages:). Whether the message has succeeded or failed in sending, the result will be delivered to the messageCollection(_:context:channel:updatedMessages:). Thus, in local caching, it is not necessary to manually send the callback from resendMessage() to the handleSendMessageResponse() as in SyncManager.
Note: Don't add the pending, succeeded or failed message objects of the resendMessage() callback to your message list data source. This can cause duplicate messages in the chat view.
// User Message
channel.resendUserMessage(with: userMessage, completionHandler: nil)
// File Message
let params = failedFileMessage.getParams()
channel.resendFileMessage(with: failedFileMessage, binaryData: params?.file, completionHandler: nil)
// Pending Messages
let pendingMessageList = messageCollection?.getPendingMessages()
// This should be called after the messageCollection.start.(with:cacheResultHandler:apiResultHandler:).
// Failed Messages
let failedMessageList = messageCollection?.getFailedMessages()
// This should be called after the messageCollection.start.(with:cacheResultHandler:apiResultHandler:).
// User Message
[channel resendUserMessageWithMessage:userMessage completionHandler:nil];
// File Message
SBDFileMessageParams *params = [failedFileMessage getFileMessageParams];
[channel resendFileMessageWithMessage:failedFileMessage binaryData:params.file completionHandler:nil];
// Pending Messages
NSArray<SBDBaseMessage *> *pendingMessageList = [self.messageCollection getPendingMessages];
// This should be called after the
// startCollectionWithInitPolicy:cacheResultHandler:cacheResultHandler: of SBDMessageCollection.
// Failed Messages
NSArray<SBDBaseMessage *> *failedMessageList = [self.messageCollection getFailedMessages];
// This should be called after the
// startCollectionWithInitPolicy:cacheResultHandler:cacheResultHandler: of SBDMessageCollection.
To update a message using SyncManager, the message delivery status needs to be manually set through the messageCollection?.updateMessage(updatedMessage) to let SyncManager know that a message has been successfully delivered. In local caching, however, updating messages is handled internally, and the result is delivered to the messageCollection(_:context:channel:updatedMessages:).
In SyncManager, the process of delivering the status of message deletion through the deleteMessage() is required. However, this process isn't needed in local caching.
Deleting a failed message.
The process is the same for both SyncManager and local caching where the failed message object is deleted explicitly from the local cache. In SyncManager, deleting a failed message is done through the deleteMessage(). In local caching, the same can be done through the removeFailedMessages().
// 1. Deleting a succeeded message. Delete from the channel object. The result is delivered to
//the SBDMessageCollectionDelegate.messageCollection(_:context:channel:deletedMessages:).
channel.delete(message, completionHandler: nil)
// 2-1. Deleting a failed message. Delete from the collection only.
messageCollection?.removeFailedMessages([message], completionHandler: { requestIds, error in
})
// 2-2. Deleting all failed messages. Delete from the collection only.
messageCollection?.removeAllFailedMessages(completionHandler: { error in
// All failed messages are deleted.
})
// 1. Deleting a succeeded message. Delete from the channel object. The result is delivered to
//the messageCollection:context:channel:deletedMessages: of SBDMessageCollectionDelegate.
[channel deleteMessage:message completionHandler:nil];
// 2-1. Deleting a failed message. Delete from the collection only.
[self.messageCollection removeFailedMessages:@[message] completionHandler:^(NSArray<NSString *> * _Nullable requestIds, SBDError * _Nullable error) {
}];
// 2-2. Deleting all failed messages. Delete from the collection only.
[self.messageCollection removeAllFailedMessagesWithCompletionHandler:^(SBDError * _Nullable error) {
// All failed messages are deleted.
}];
SyncManager's SBDMessageCollection has a remove() method that clears all the messages managed by the collection and stops the synchronization processes in the collection instance.
On the other hand, local caching uses the dispose() method to clear the existing chat view. You should call this method when the current user leaves the channel or you need to create a new collection because a huge gap has been detected by the didDetectHugeGap:.
SyncManager's messageCollection?.messageCount() is used to count the number of succeeded messages in the collection. In local caching, it should be updated as shown in the code below.
// A message collection provides a getter for the current message list in the message collection.
let succeededMessageList = messageCollection?.getSucceededMessages()
let messageCount = succeededMessageList?.count
// A message collection provides a getter for the current message list in the message collection.
NSArray<SBDBaseMessage *> *succeededMessageList = [self.messageCollection getSucceededMessages];
NSUInteger messageCount = succeededMessageList.count;