Home
/
Chat
/
iOS

Local caching

Local caching enables Sendbird Chat SDK for iOS to locally cache and retrieve group channel and message data. This facilitates offline messaging by allowing the SDK to create a channel list view or a chat view in a prompt manner and display them even when a client app is in offline mode.

Note: Using local caching should be determined when you're initializing the Chat SDK. See Initialize with APP_ID and Connect to Sendbird server in the Authentication page to learn how to enable local caching for the client app.

Local caching operates on the SBDGroupChannelCollection and SBDMessageCollection classes, which are used to build a channel list and a chat view, respectively.

Note: Even when you are not using local caching, you can still build the client app with the collections.

Unlike SyncManager that listens to any data update, collections are designed to react to the events that can cause change in a view. In the Chat SDK, an Event controller oversees such events and passes them to a relevant collection. For example, if a new group channel is created while the current user is looking at a chat view, the current view won't be affected by this event.

Note: Understand the differences between local caching and SyncManager, and learn how the Migration should be done.


SBDGroupChannelCollection

A SBDGroupChannelCollection is composed of the following elements.

SwiftObjective-C
import SendBirdSDK

class ChannelListViewController: UIViewController {
    var collection: SBDGroupChannelCollection?

    // ...

    // Create a GroupChannelListQuery instance first.
    func createGroupChannelCollection() {
        let query = SBDGroupChannel.createMyGroupChannelListQuery()
        query?.includeEmptyChannel = true
        query?.order = .chronological

        // Create a SBDGroupChannelCollection instance.
        self.collection = SBDGroupChannelCollection(query: query)
        self.collection?.delegate = self
    }

    // Load channels.
    func loadMore() {
        if let collection = self.collection {
            if collection.hasMore {
                collection.loadMore { channels, error in
                    if error != nil {
                        // Handle error.
                        return
                    }
                }
            }
        }
    }

    // Clear the collection.
    func dispose() {
        if let collection = self.collection {
            collection.dispose()
        }
    }
}

extension ChannelListViewController: SBDGroupChannelCollectionDelegate {
    func channelCollection(_ collection: SBDGroupChannelCollection, context: SBDChannelContext, addedChannels channels: [SBDGroupChannel]) {
        // ...
    }

    func channelCollection(_ collection: SBDGroupChannelCollection, context: SBDChannelContext, updatedChannels channels: [SBDGroupChannel]) {
        // ...
    }

    func channelCollection(_ collection: SBDGroupChannelCollection, context: SBDChannelContext, deletedChannelUrls: [String]) {
        // ...
    }
}

When the createGroupChannelCollection() method is called, the group channel data stored in the local cache and Sendbird server are fetched and sorted based on the values in the SBDGroupChannelListQuery. Also, the SBDGroupChannelCollectionDelegate allows you to set the event listeners that can subscribe to channel-related events when creating the collection.

As for pagination, the hasMore() will check if there are more group channels to load whenever a user hits the bottom of the channel list. If true, the loadMore() will retrieve channels from the local cache and the remote server to show on the list.

To learn more about the collection and how to create a channel list view with it, see Group channel collection.


SBDMessageCollection

A SBDMessageCollection is composed of the following elements.

SwiftObjective-C
import SendBirdSDK

class ChatViewController: UIViewController {
    var collection: SBDMessageCollection?
    var channel: SBDGroupChannel?
    var startingPoint: UInt64?

    // ...

    // Create a MessageCollection instance.
    func createMessageCollection() {
        // You can use a SBDMessageListParams instance for the SBDMessageCollection.
        let params = SBDMessageListParams()
        params.reverse = false
        // ...

        self.collection = SBDMessageCollection(channel: self.channel, startingPoint: self.startingPoint, params: params)
        self.collection?.delegate = self
    }

    // Initialize messages from the startingPoint.
    func initialize() {
        self.collection?.start(with: .cacheAndReplaceByApi, cacheResultHandler: { messages, error in
            // Messages will be retrieved from the local cache.
        }, apiResultHandler: { messages, error in
            // Messages will be retrieved from the Sendbird server through API.
            // According to the SBDMessageCollectionInitPolicy.cacheAndReplaceByApi,
            // the existing data source needs to be cleared
            // before adding retrieved messages to the local cache.
        })
    }

    // Load next messages.
    func loadNext() {
        if let collection = self.collection {
            if collection.hasNext {
                collection.loadNext { messages, error in
                    if error != nil {
                        // Handle error.
                        return
                    }
                }
            }
        }
    }

    // Load previous messages.
    func loadPrevious() {
        if let collection = self.collection {
            if collection.hasPrevious {
                collection.loadPrevious { messages, error in
                    if error != nil {
                        // Handle error.
                        return
                    }
                }
            }
        }
    }

    // Clear the collection.
    func dispose() {
        if let collection = self.collection {
            collection.dispose()
        }
    }
}

extension ChatViewController: SBDMessageCollectionDelegate {
    func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, addedMessages messages: [SBDBaseMessage]) {
        switch context.messageSendingStatus {
        case .succeeded:
        case .pending:
        default:
        }
    }

    func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, updatedMessages messages: [SBDBaseMessage]) {
        switch context.messageSendingStatus {
        case .succeeded:
        case .pending:
        case .failed:
        case .canceled:
        default:
        }
    }

    func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, channel: SBDGroupChannel, deletedMessages messages: [SBDBaseMessage]) {
        switch context.messageSendingStatus {
        case .succeeded:
        case .failed:
        default:
        }
    }

    func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, updatedChannel channel: SBDGroupChannel) {
    }

    func messageCollection(_ collection: SBDMessageCollection, context: SBDMessageContext, deletedChannel channelUrl: String) {
    }

    func didDetectHugeGap(_ collection: SBDMessageCollection) {
        // This is called when there're more than 300 messages are missing in the local cache compared to the remote server.

        // Clear the collection.
        self.collection.dispose();

        // Create a new message collection object.
        self.createMessageCollection();

        // An additional implementation is required for initialization.
    }
}

In a SBDMessageCollection, the initialization is dictated by the initialization policy. This determines which data to use for the collection. Currently, we only support SBDMessageCollectionInitPolicyCacheAndReplaceByApi. According to this, the collection will load the messages stored in the local cache through the cacheResultHandler(). Then the apiResultHandler() will be called to replace them with the messages fetched from Sendbird server through an API request.

As for pagination, the hasNext will check if there are more messages to load whenever a user hits the bottom of the chat view. If true, the loadNextWithCompletionHandler: method will retrieve those messages. The loadPrevious works the same way when the scroll reaches the oldest message in the view.

To learn more about the collection and how to create a chat view with it, see Message collection.


Gap and synchronization

Depending on the connection status, the client app may not load new events properly. This will create a gap between the data in the local cache and Sendbird server. A gap is a chunk of messages or channels that exist in the remote server but are missing in the local cache.

To prevent such a gap, background sync constantly communicate with Sendbird server and fetches data from it. When it pulls data, the synchronization starts from the latest message to old messages. This ensures that the local cache has all the data in order. However, because this synchronization takes place in the background, changes won't call collection event handlers or affect the view.

Despite background sync, a gap can still happen. When the client app is opened and in the foreground, the SDK checks with Sendbird server to see if there has been a gap. If more than 300 messages are missing in the local cache compared to that of in the remote server, Sendbird Chat SDK determines such as a huge gap and the didHugeGapDetected: is called. In this case, it is more effective to discard the existing message collection and create a new one. On the other hand, relatively a small gap can be filled in through changelog sync.

Changelog sync

While background sync makes sure that the local cache keeps its data up-to-date compared to Sendbird server, changelog sync fetches any events that can affect the channel list view and the chat view.

Changelog sync pulls data when the client app is back online. When the client app is connected the server, it fetches events in chronological order of the updated_at value, from first to last. Events fetched by changelog sync will be added to the collection, updating the view.

The events will be delivered to the collection by event delegates such as the messageCollection:context:channel:addedMessages:, messageCollection:context:channel:updatedMessages:, messageCollection:context:channel:deletedMessages:, messageCollection:context:updatedChannel:, and messageCollection:context:deletedChannel:.


Auto resend

Messages are normally sent through the WebSocket connection which means the connection must be secured and confirmed before sending any message. With local caching, you can temporarily keep unsent messages in local cache if the WebSocket connection is lost. The Chat SDK with local caching marks the failed messages as pending, locally stores them, and automatically resends the pending messages when the WebSocket is reconnected. This is called auto resend.

The following cases are eligible for auto resend when:

  • A user message couldn't be sent because the WebSocket connection is lost even before it is established.

  • A file message couldn't upload the attached file to Sendbird server.

  • An attached file was uploaded to Sendbird server but the file message itself couldn't be sent because the WebSocket connection is closed.

User messages

User messages are sent through the WebSocket. When messages couldn't be sent because the WebSocket connection is lost, the Chat SDK will receive an error through a callback and queue the pending messages in the local cache for auto resend. When the client app is reconnected, the SDK will attempt to resend the messages.

If the message is successfully sent, the client app will receive a response from Sendbird server. Then the messageCollection:context:channel:updatedMessages: is called to change the pending message to the sent message in the data source and update the chat view.

File messages

File messages can be sent through either the WebSocket connection or an API request. Sending file messages requires the following two steps.

First, the attached file must be uploaded to Sendbird server as an HTTP request. To do so, the Chat SDK checks the status of your network connection. If the network is not connected, the file can't be uploaded to the server. In this case, the SDK will handle the file message as a pending message and put it in a queue for auto resend.

If the network is connected and the file is successfully uploaded to the server, its URL will be delivered in a response and the SDK will replace the file with its URL string. At first, the SDK attempts to send the message through the WebSocket. If the WebSocket connection is lost, the client app checks the network connection once again to make another HTTP request for the message. If the SDK detects the network as disconnected, it will get an error code that marks the message as a pending message, allowing the message to be automatically resent when the network is reconnected.

On the other hand, if the network is connected but the HTTP request fails, the message isn't eligible for auto resend.

If the message is successfully sent, the client app will receive a response from Sendbird server. Then the messageCollection:context:channel:updatedMessages: is called to change the pending message to the sent message in the data source and update the chat view.

Failed messages

If messages weren't sent due to other error cases, such as the network trouble, the messageCollection:context:channel:updatedMessages: will be called to re-label the pending messages as failed messages. Those messages can't be queued for auto resending. The following show some of those errors.

SBDErrorNetworkError
SBDErrorAckTimeout

Note: The pending messages can last in the queue only for three days. If the WebSocket connection is back online after three days, the messageCollection:context:channel:updatedMessages: will be called to mark any 3-day-old pending messages as failed messages.


Other methods

The following codes show a list of methods that can help you leverage the local caching functionalities.

The getCachedDataSize will let you know the size of the data stored in the local cache. If you want to erase the whole data, use the clearCachedData. Meanwhile, the clearCachedMessages allows you to get rid of unnecessary messages from the local cache by specifying the channel URL of those messages.

The getUserMessageParams and getFileMessageParams can be used when drawing pending messages in the chat view. This method will return the same UserMessageParams and FileMessageParams that you used when sending the messages.

Note: These methods are available in Objective-C only.

// SBDMain
+ (NSUInteger)getCachedDataSize;
+ (void)clearCachedMessages:(nonnull NSString *)channelUrl
          completionHandler:(nullable void (^)(SBDError * _Nullable error))completionHandler;
+ (void)clearCachedDataWithCompletionHandler:(nullable void (^)(void))completionHandler;

// SBDUserMessage
- (nullable SBDUserMessageParams *)getUserMessageParams; // This is used for auto resend or when creating a new collection due to a huge gap.

// SBDFileMessage
- (nullable SBDFileMessageParams *)getFileMessageParams; // This is used for auto resend or when creating a new collection due to a huge gap.