/ SDKs / iOS
SDKs
Chat SDKs iOS v4
Chat SDKs iOS
Chat SDKs
iOS
Version 4

Local caching

Copy link

Local caching enables Sendbird Chat SDK for iOS to locally cache and retrieve group channel and message data. Its benefits include reducing refresh time and allowing a client app to create a channel list or a chat view that can work online as well as offline, which can be used for offline messaging.

You can enable local caching when initializing the Chat SDK by setting initParams. See Initialize the Chat SDK with APP_ID and Connect to the Sendbird server with a user ID to learn more.

Local caching relies on the GroupChannelCollection and MessageCollection classes, which are used to build a channel list view and a chat view, respectively. Collections are designed to react to events that can cause changes in a channel list or chat view. An event controller in the SDK 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: You can use these collections even when building your app without local caching.


Functionalities by topic

Copy link

The following is a list of functionalities our Chat SDK supports.

Using group channel collection

Copy link
FunctionalityDescriptionOpen channelGroup channel

Group channel collection

Keeps the client app's channel list synced with that on the Sendbird server.

Using message collection

Copy link
FunctionalityDescriptionOpen channelGroup channel

Message collection

Keeps the client app's chat view synced with that on the Sendbird server.


GroupChannelCollection

Copy link

GroupChannelCollection allows you to swiftly create a channel list view that stays up to date on all channel-related events. The GroupChannelCollection class is composed of the following functions and variables.

import SendbirdChatSDK

class CustomViewController: UIViewController {
    var collection: GroupChannelCollection?

    // ...

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    // Create a GroupChannelListQuery instance first.
    func createGroupChannelCollection() {
        let query = GroupChannel.createMyGroupChannelListQuery { params in
            params.includeEmptyChannel = true
            params.order = .chronological
            params.includeMemberList = true
            params.includeMetaData = true
            params.includeFrozenChannel = true
        }

        // Create a GroupChannelCollection instance.
        self.collection = SendbirdChat.createGroupChannelCollection(query: query)
        self.collection?.delegate = self
    }

    // Load channels.
    func loadMore() {
        guard let collection = self.collection else {
            return
        }
        
        if collection.hasNext {
            collection.loadMore(completionHandler: { channels, error in
                guard error == nil else {
                    // Handle error.
                    return
                }
            })
        }
    }

    // Clear the collection.
    func dispose() {
        self.collection?.dispose()
    }
}

extension ChannelListViewController: GroupChannelCollectionDelegate {
    func channelCollection(_ collection: GroupChannelCollection, context: ChannelContext, addedChannels channels: [GroupChannel]) {
        // ...
    }

    func channelCollection(_ collection: GroupChannelCollection, context: ChannelContext, updatedChannels channels: [GroupChannel]) {
        // ...
    }

    func channelCollection(_ collection: GroupChannelCollection, context: ChannelContext, deletedChannelURLs: [String]) {
        // ...
    }
}

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

As for pagination, hasNext checks if there are more group channels to load whenever a user hits the bottom of the channel list. If so, loadMore(completionHandler:) retrieves channels from the local cache and the Sendbird server to display on the list.

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


MessageCollection

Copy link

MessageCollection allows you to swiftly create a chat view that includes all data. The MessageCollection class is composed of the following functions and variables.

import SendbirdChatSDK

class CustomViewController: UIViewController {
    var collection: MessageCollection?
    var channel: GroupChannel?
    var startingPoint: Int64 = Int64.max

    // ...
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    // Create a MessageCollection instance.
    func createMessageCollection() {
        guard let channel = self.channel else {
            return
        }
        
        // You can use a MessageListParams instance for MessageCollection.
        let params = MessageListParams()
        params.reverse = false
        params.isInclusive = false
        params.messageTypeFilter = .all
        params.includeMetaArray = false
        params.includeReactions = false
        // ...

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

    // Initialize messages from startingPoint.
    func initialize() {
        guard let collection = self.collection else {
            return
        }
        
        collection.startCollection(initPolicy: .cacheAndReplaceByApi, cacheResultHandler: { messages, error in
            // Messages are retrieved from the local cache.
        }, apiResultHandler: { messages, error in
            // Messages are retrieved from the Sendbird server through the API.
            // According to MessageCollectionInitPolicy.cacheAndReplaceByApi,
            // the existing data source needs to be cleared
            // before adding retrieved messages to the local cache.
        })
    }

    // Load the next set of messages.
    func loadNext() {
        guard let collection = self.collection else {
            return
        }
        
        if collection.hasNext {
            collection.loadNext { messages, error in
                guard error == nil else {
                    return // Handle error.
                }
            }
        }
    }

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

    // Clear the collection.
    func dispose() {
        self.collection?.dispose()
    }
}

extension CustomViewController: MessageCollectionDelegate {
    func messageCollection(_ collection: MessageCollection, context: MessageContext, channel: GroupChannel, addedMessages messages: [BaseMessage]) {
        switch context.sendingStatus {
        case .succeeded:
            // ...
        case .pending:
            // ...
        default:
            // ...
        }
    }

    func messageCollection(_ collection: MessageCollection, context: MessageContext, channel: GroupChannel, updatedMessages messages: [BaseMessage]) {
        switch context.sendingStatus {
        case .succeeded:
            // ...
        case .pending:
            // ...
        case .failed:
            // ...
        case .canceled:
            // ...
        default:
            // ...
        }
    }

    func messageCollection(_ collection: MessageCollection, context: MessageContext, channel: GroupChannel, deletedMessages messages: [BaseMessage]) {
        switch context.sendingStatus {
        case .succeeded:
            // ...
        case .failed:
            // ...
        default:
            // ...
        }
    }

    func messageCollection(_ collection: MessageCollection, channelContext: ChannelContext, updatedChannel channel: GroupChannel) {
    }

    func messageCollection(_ collection: MessageCollection, channelContext: ChannelContext, deletedChannel channelURL: String) {
    }

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

        // Clear the collection.
        collection.dispose()

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

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

In the MessageCollection class, the initialization is dictated by the initialization policy. This determines which data to use for the collection. Currently, we only support .cacheAndReplaceByApi. According to this policy, the collection loads the messages stored in the local cache through cacheResultHandler. Then apiResultHandler is called to replace them with the messages fetched from the Sendbird server through an API request.

As for pagination, hasNext checks if there are more messages to load whenever a user hits the bottom of the chat view. If so, the loadNext(completionHandler:) method retrieves those messages. The loadPrevious(completionHandler:) method works the same way when the scroll reaches the least recent message in the chat view.

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


Gap and synchronization

Copy link

A gap is created when messages or channels that exist on the Sendbird server are missing from the local cache. Such discrepancy occurs when a client app isn't able to properly load new events due to connectivity issues. To prevent such a gap, the Chat SDK constantly communicates with the Sendbird server and fetches data through synchronization, pulling a chunk of messages before and after startingPoint.

Such gap can significantly widen if a user has been offline for a long time. If more than 300 messages are missing in the local cache compared to the Sendbird server, Sendbird Chat SDK classifies this as a huge gap and didHugeGapDetected(_:) is called. In case of a huge gap, it is more effective to discard the existing message collection and create a new one. On the other hand, a relatively small gap created when the SDK was offline can be filled in through changelog sync.


Database management

Copy link

You can configure how much space in local cache Sendbird Chat SDK can use for its database and in which order to clear the cached data when it reaches the limit. The LocalCacheConfig class helps you manage the local cache settings and its instance can be passed in InitParams during SDK initialization.

LocalCacheConfig

Copy link

The LocalCacheConfig class has two properties related to database management, which are maxSize and clearOrder.

The maxSize property sets the size limit for cached data. The default value of maxSize is 256 MB, but you can choose a value between 64 MB and Int64.max. Once the database reaches its limit, the SDK starts to clear cached messages according to clearOrder.

Note: The minimum value for maxSize is 64 MB. If you specify a value lower than 64 MB, the value is systematically changed to 64MB.

The clearOrder property determines which data to clear first when local cache hits the limit. You can use the default setting, messageCollectionAccessedAt, to clear cached messages of a group channel based on the time when the current user accessed the channel's MessageCollection instance. The longer the current user hasn't accessed the instance, the sooner it is removed. Or you can set it to custom and customize the settings with customClearOrderComparator. You can use CachedBaseChannelInfo for the comparator to determine which cached data to clear first.

Note: If clearOrder is set to custom but customClearOrderComparator isn't implemented, the SDK uses the default value of messageCollectionAccessedAt instead.

let config = LocalCacheConfig()
config.maxSize = 256
config.clearOrder = .custom
config.customClearOrderComparator = { (lhs, rhs) in
    return NSNumber(value: lhs.cachedMessageCount).compare(NSNumber(value: rhs.cachedMessageCount)) // CachedBaseChannelInfo contains the group channel's info such as cachedMessageCount.
}
let initParam = InitParams(applicationId: "{APP_ID}", isLocalCachingEnabled: true, logLevel: .verbose, appVersion: "x.y.z")
    initParam.localCacheConfig = config

SendbirdChat.initialize(params: initParam)

How it works

Copy link

Once LocalCacheConfig is set, Sendbird Chat SDK checks the size of cached data through getCachedDataSize() when the connect() method is called with the current user's user_id for the first time after the SDK initialization. When the cached database size reaches its maxSize value, clearCachedMessages() is called to clear messages in the database according to clearOrder as explained above. You can use either the default setting, messageCollectionAccessedAt, or your own custom order to clear data in local cache.

If you wish to clear all cached data at once, use clearCachedData().


Data encryption

Copy link

Data in local cache can be encrypted for data protection and security. By default, the Chat SDK doesn't encrypt the cached data. However, iOS offers four different levels of data protection at the system level and the Chat SDK supports two types, which are FileProtectionType.completeUntilFirstUserAuthentication and FileProtectionType.completeUnlessOpen. The systematic default is completeUntilFirstUserAuthentication, which ensures that the data is stored in an encrypted format on disk and can be accessed only after the device has booted. Meanwhile, completeUnlessOpen provides stronger data protection, keeping the data in an encrypted format once it's closed.

If you set the LocalCacheConfig.isEncryptionEnabled parameter to false during the Chat SDK initialization, the default type is applied. When isEncryptionEnabled is set to true, data in local cache is encrypted and protected at the higher level of completeUnlessOpen.

let localCacheConfig = LocalCacheConfig(isEncryptionEnabled: true)  // The higher level of data protection is applied,
                                                                    // which is completeUnlessOpen.
let initParams = InitParams(
    applicationId: appId,
    isLocalCachingEnabled: true,
    localCacheConfig: localCacheConfig
)

Auto resend

Copy link

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

The following cases are eligible for auto resend.

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

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

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

User message

Copy link

A user message is sent through the WebSocket. If a message couldn't be sent because the WebSocket connection was lost, the Chat SDK receives an error through a callback and queues the pending message in the local cache for auto resend. When the client app is reconnected, the SDK then attempts to resend the message.

If the message is successfully sent, the client app receives a response from the Sendbird server. Then messageCollection(_:context:channel:updatedMessages:) is called to change the pending message to a sent message in the data source and updates the chat view.

File message

Copy link

A file message can be sent through either the WebSocket connection or an API request.

When sending a file message, the attached file must be uploaded to the Sendbird server as an HTTP request. To do so, the Chat SDK checks the status of the network connection. If the network isn't connected, the file can't be uploaded to the server. In this case, the SDK handles the file message as a pending message and adds to the queue for auto resend.

If the network is connected and the file is successfully uploaded to the server, its URL is delivered in a response and the SDK replaces 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 gets 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 receives a response from the Sendbird server. Then messageCollection(_:context:channel:updatedMessages:) is called to change the pending message to a sent message in the data source and updates the chat view.

Failed message

Copy link

If a message couldn't be sent due to some other error, messageCollection(_:context:channel:updatedMessages:) is called to re-label the pending message as a failed message. Messages labeled as such can't be queued for auto resend. The following shows some of these errors.

ChatError.networkError
ChatError.ackTimeout

Note: A pending message can last in the queue only for three days. If the WebSocket connection is back online after three days, the messageCollection(_:context:channel:updatedMessages:) method is called to mark any three-day-old pending messages as failed messages.


Other methods

Copy link

The following code block shows a list of methods that can help you leverage the local caching functionalities.

The getCachedDataSize() method lets you know the size of the data stored in the local cache. If you want to erase the entire data, use the clearCachedData(completionHandler:) method. Meanwhile, the clearCachedMessages(channelURL:completionHandler:) method lets you get rid of unnecessary messages from the local cache by specifying the channel URL of those messages.

The messageParams property of the BaseMessage class can be used when drawing pending messages in the chat view.

public class SendbirdChat {
    public class func getCachedDataSize() -> Int
    public class func clearCachedData(completionHandler SBErrorHandler?)
    public class func clearCachedMessages(channelURL: String, completionHandler: SBErrorHandler?)
}

public class BaseMessage {
    // This is used for auto resend.
    // or when creating a new collection due to a huge gap.
    public var messageParams: BaseMessageCreateParams?
}