Sendbird SyncManager for iOS
Table of contents
Introduction
Sendbird SyncManager for iOS is a Chat SDK add-on that optimizes the user caching experience by interlinking the synchronization of the local data storage with the chat data in Sendbird server through an event-driven structure.
How it works
SyncManager leverages local caching and synchronizes the chat data between the local storage and Sendbird server. By handling the operations in an event-driven structure, the add-on provides a simplified Chat SDK integration and a better user experience.
Operations
- Background sync occurs whenever there is a connection and automatically stores data fetched from Sendbird server into the local cache.
- Real time sync occurs all the time; it identifies, stores, and delivers the real-time events received from WebSocket connection.
- Offline mode ensures your client app is operational during offline mode, meaning that even without background sync, the view can display cached data.
More about Sendbird SyncManager for iOS
Find out more about Sendbird SyncManager for iOS on SyncManager for iOS doc. If you have any comments or questions regarding bugs and feature requests, visit Sendbird community.
Before getting started
This section shows the prerequisites you need to check to use Sendbird SyncManager for iOS.
Requirements
The minimum requirements for SyncManager for iOS are:
- iOS 8.0+
- Sendbird Chat SDK for iOS v3.0.178+
Getting started
This section gives you information you need to get started with Sendbird SyncManager for iOS.
Try the sample app
Download the sample app to test the core features of SyncManager for iOS.
Note: The fastest way to test our SyncManager is to build your chat app on top of our sample app. Make sure to change the application ID of the sample app to your own. Go to the Create a Sendbird application from your dashboard section to learn more.
Install SendBirdSyncManager framework from CocoaPods
Add below into your Podfile on Xcode.
platform :ios, '8.0'
use_frameworks!
target YOUR_PROJECT_TARGET do
pod 'SendBirdSyncManager'
end
Install SendBirdSyncManager
framework through CocoaPods
.
pod install
Update SendBirdSyncManager
framework through CocoaPods
.
pod update SyncManager
Now you can see installed SendBirdSyncManager
framework by inspecting YOUR_PROJECT.xcworkspace
.
Note:
SendBirdSyncManager
is dependent withSendBird SDK
. If you installSendBirdSyncManager
,Cocoapods
automatically installSendBird SDK
as well. And the minimum version ofSendBird SDK
is 3.0.203.
Install SendBirdSyncManager framework from Carthage
- Add
github "sendbird/sendbird-syncmanager-ios"
to yourCartfile
. - Run
carthage update
. - Go to your Xcode project's General settings. Open
<YOUR_XCODE_PROJECT_DIRECTORY>/Carthage/Build/iOS
in Finder and dragSendBirdSyncManager.framework
to the Embedded Binaries section in Xcode. Make sureCopy items if needed
is selected and click Finish.
Implementation guide
Initialization
SBSMSyncManager
is a singlton class. And when SBSMSyncManager
was initialized, a instance for Database
is set up. So if you want to initialize Database
as soon as possible, call setup(_:)
first just after you get a user's ID. we recommend it is in application(_:didFinishLaunchingWithOptions:)
.
// swift
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// after getting user's ID or login
SBSMSyncManager.setup(withUserId: userId)
}
// objective-c
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// after getting user's ID or login
[SBSMSyncManager setupWithUserId:userId];
}];
Collection
Collection
is a container to manage Sendbird objects(SBDGroupChannel
, SBDBaseMessage
) related to a view. SBSMChannelCollection
is attached to channel list view contoller and SBSMMessageCollection
is attached to message list view contoller accordingly. The main purpose of Collection
is,
- To listen data event and deliver it as view event.
- To fetch data from cache or Sendbird server and deliver the data as view event.
Each collection has event subscriber and data fetcher. Event subscriber listens to data events so that it could apply these data updates into view, while data fetcher loads data from cache or server and sends the data to an event handler.
- Channel collection
Channel is a mutable data where chat is active. There are frequent updates on the channel's last message unread message count and also drastic changes in the position of each channel since many apps sort channels by the most recent message. For that reason, SBSMChannelCollection
depends mostly on server sync. Here's the process SBSMChannelCollection
synchronizes data:
- It loads channels from cache and the view shows them.
- Then it fetches the most recent channels from Sendbird server and merges with the channels in view.
- It fetches from Sendbird server every time
fetch(_:)
is called in order to view previous channels.
Note: Channel data sync mechanism could change later.
SBSMChannelCollection
requires SBDGroupChannelListQuery
instance of Sendbird SDK as it binds the query into the collection. Then the collection filters data with the query. Here's the code to create new SBSMChannelCollection
instance. The creation of channel collection is usually in viewDidLoad()
of group channel list view controller.
// swift
override func viewDidLoad() {
let query: SBDGroupChannelListQuery? = SBDGroupChannel.createMyGroupChannelListQuery()
// limit, order, ... setup your query here.
let channelCollection: SBSMChannelCollection? = SBSMChannelCollection.init(query: query)
self.channelCollection? = channelCollection // Recommands to set a property of view controller
}
// objective-c
- (void)viewDidLoad {
SBDGroupChannelListQuery *query = [SBDGroupChannel createMyGroupChannelListQuery];
// limit, order, ... setup your query here.
SBSMChannelCollection *channelCollection = [SBSMChannelCollection collectionWithQuery:query];
self.channelColletion = channelCollection; // Recommands to set a property of view controller
}
If the view is closed, which means the collection is obsolete and no longer used, remove collection explicitly. In viewcontroller, it will be in deinit
(dealloc
).
// swift
deinit {
channelCollection?.delegate = nil
channelCollection?.remove()
}
// objective-c
- (void)dealloc {
if (channelCollection != nil) {
channelCollection.delegate = nil;
}
[channelCollection remove];
}
SBSMChannelCollection
provides event handlers with delegates. An event handler is named as SBSMChannelCollectionDelegate
and it receives SBSMChannelEventAction
and list of channels
with the arrival of an event. The SBSMChannelEventAction
is a keyword to notify what happened to the channel list, and the channel
is a type of SBDGroupChannel
instance. You can create an view controller instance and implement the event handler and add it to the collection.
// swift
// add delegate
import SendBirdSyncManager
class GroupChannelListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SBSMChannelCollectionDelegate {
override func viewDidLoad() {
// ...
channelCollection?.delegate = self
// ...
}
// channel collection delegate
func collection(_ collection: SBSMChannelCollection, didReceiveEvent action: SBSMChannelEventAction, channels: [SBDGroupChannel]) {
switch (action) {
case SBSMChannelEventAction.insert:
// Insert channels on list
break
case SBSMChannelEventAction.update:
// Update channels of list
break
case SBSMChannelEventAction.remove:
// Remove channels of list
break
case SBSMChannelEventAction.move:
// Move channel of list
break
case SBSMChannelEventAction.clear:
// Clear(Remove all) channels
break
case SBSMChannelEventAction.none:
break
default:
break
}
}
}
// objective-c
// add delegate
#import <SendBirdSyncManager/SendBirdSyncManager.h>
@interface GroupChannelListViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, SBSMChannelCollectionDelegate>
@end
@implementation GroupChannelListViewController
- (void)viewDidLoad {
channelCollection.delegate = self;
// ..
}
// channel collection delegate
- (void)collection:(SBSMChannelCollection *)collection didReceiveEvent:(SBSMChannelEventAction)action channels:(NSArray<SBDGroupChannel *> *)channels {
guard collection == self.channelCollection, channels.count > 0 else {
return
}
switch (action) {
case SBSMChannelEventActionInsert: {
// Insert channels on list
break;
}
case SBSMChannelEventActionUpdate: {
// Update channels of list
break;
}
case SBSMChannelEventActionRemove: {
// Remove channels of list
break;
}
case SBSMChannelEventActionMove: {
// Move channel of list
break;
}
case SBSMChannelEventActionClear: {
// Clear(Remove all) channels
break;
}
case SBSMChannelEventActionNone:
default: {
break;
}
}
}
- Data fetcher
Fetched channels would be delivered to the delegate method. The fetcher determines the SBSMChannelEventAction
automatically so you don't have to consider duplicated data in view. Generally fetch(_:)
is called when view was created and the user requests the next page of the channel list and also wants to refresh the channel list.
// swift
override viewDidLoad() {
channelCollection.fetch(completionHandler: {(error) in
// This callback is optional and useful to catch the moment of loading ended.
})
}
func refreshChannel() {
// begin loading progress
channelCollection?.remove()
channelCollection? = nil
// create channel collection
channelCollection?.fetch(completionHandler: { (error) in
// end load progress
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// .. dequeue reusable cell
if self.channels.count > 0 && indexPath.row + 1 == self.channels.count {
channelCollection?.fetch(completionHandler: { (error) in
// end load progress
})
}
// ...
}
// objective-c
- (void)viewDidLoad {
[channelCollection fetchWithCompletionHandler:^(SBDError * _Nullable error) {
// This callback is optional and useful to catch the moment of loading ended.
}];
}
- (void)refreshChannel {
// begin loading progress
[channelCollection remove];
channelCollection = nil;
// create channel collection
[channelCollection fetchWithCompletionHandler:^(SBDError * _Nullable error) {
// end loading progress
}];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// .. dequeue reusable cell
if (self.channels.count > 0 && indexPath.row + 1 == self.channels.count) {
// start loading progress
[self.channelCollection fetchWithCompletionHandler:^(SBDError * _Nullable error) {
// end loading progress
}];
}
// ...
}
- Message collection
Message is relatively static data and SyncManager supports full-caching for messages. SBSMMessageCollection
conducts background synchronization so that it synchronizes all the messages until it reaches to the first message. Background synchronization DOES NOT affect view directly but store it for local cache. For view update, explicitly call fetch(_:_:)
with direction, which fetches data from cache and sends the data into collection handler.
If the synchronization is done or a synchronization request is failed, background synchronization ceases.
Note: Background synchronization run in background thread.
For various viewpoint(viewpointTimestamp
) support, SBSMMessageCollection
sets a timestamp for when to fetch messages. The viewpointTimestamp
is a timestamp to start background synchronization in both previous and next direction (and also the point where a user sees at first). Here's the code to create SBSMMessageCollection
.
The creation of message collection is usually in viewDidLoad()
of message list view controller as well as in the channel collection.
// swift
override viewDidLoad() {
// ...
let filter: SBSMMessageFilter = SBSMMessageFilter.init(messageType: SBDMessageTypeFilter, customType: customTypeFilter, senderUserIds: senderUserIdsFilter)
let viewpointTimestamp: Int64 = getLastReadTimestamp()
// or LONG_LONG_MAX if you want to see the most recent messages
let messageCollection: SBSMMessageCollection? = SBSMMessageCollection.init(channel: channel, filter: filter, viewpointTimestamp: viewpointTimestamp)
// ...
}
// objective-c
- (void)viewDidLoad {
// ...
SBSMMessageFilter *filter = [SBSMMessageFilter filterWithMessageType:SBDMessageTypeFilter customType:customtypeFilter senderUserIds:senderUserIdsFilter];
long long viewpointTimestamp = getLastReadTimestamp();
// or LONG_LONG_MAX if you want to see the most recent messages
SBSMMessageCollection *messageCollection = [SBSMMessageCollection collectionWithChannel:self.channel filter:filter viewpointTimestamp:viewpointTimestamp];
// ...
}
You can dismiss the collection when the collection is obsolete and no longer used. It is recommended for remove()
to be in deinit
of the message view contorller.
// swift
deinit {
messageCollection?.delegate = nil
messageCollection?.remove()
}
- (void)dealloc {
if (self.messageCollection != nil) {
self.messageCollection.delegate = nil;
}
[messageCollection remove];
}
SBSMMessageCollection
has an event handler for delegates, which can be implemented and added to the collection. An event handler is named as SBSMMessageCollectionDelegate
and it receives SBSMMessageEventAction
and list of messages
with the arrival of an event. The SBSMMessageEventAction
is a keyword to notify what happened to the message, and the message
is a kind of SBDBaseMessage
instance of Sendbird SDK.
// swift
// add delegate
import SendBirdSyncManager
class GroupChannelChattingViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SBSMMessageCollectionDelegate {
override func viewDidLoad() {
// ...
messageCollection.delegate = self
// ...
}
// message collection delegate
func collection(_ collection: SBSMMessageCollection, didReceiveEvent action: SBSMMessageEventAction, messages: [SBDBaseMessage]) {
guard collection == self.messageCollection, messages.count > 0 else {
return
}
switch action {
case SBSMMessageEventAction.insert:
self.chattingView?.insert(messages: messages, completionHandler: nil)
break
case SBSMMessageEventAction.update:
self.chattingView?.update(messages: messages, completionHandler: nil)
break
case SBSMMessageEventAction.remove:
self.chattingView?.remove(messages: messages, completionHandler: nil)
break
case SBSMMessageEventAction.clear:
self.chattingView?.clearAllMessages(completionHandler: nil)
break
case SBSMMessageEventAction.none:
break
default:
break
}
}
}
// objective-c
// add delegate
#import <SendBirdSyncManager/SendBirdSyncManager.h>
@interface GroupChannelChattingViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, SBSMMessageCollectionDelegate, SBDConnectionDelegate>
@end
@implementation GroupChannelChattingViewController
- (void)viewDidLoad {
// ..
messageCollection.delegate = self;
// ..
}
// message collection delegate
- (void)collection:(SBSMMessageCollection *)collection didReceiveEvent:(SBSMMessageEventAction)action messages:(NSArray<SBDBaseMessage *> *)messages {
if (self.messageCollection != collection || messages.count == 0) {
return;
}
switch (action) {
case SBSMMessageEventActionInsert: {
//
break;
}
case SBSMMessageEventActionUpdate : {
//
break;
}
case SBSMMessageEventActionRemove: {
//
break;
}
case SBSMMessageEventActionClear: {
//
break;
}
case SBSMMessageEventActionNone:
default:
break;
}
}
SBSMMessageCollection
has a data fetcher by direction: SBSMMessageDirection.previous
and SBSMMessageDirection.next
. It only fetches data from cache and never directly requests to Sendbird server. If no more data is available in a certain direction, it internally waits for the background synchronization and fetches the synced messages right after the progression of the synchronization. When view is created, generally call fetch(_:_:)
to make users request the previous/next page of the message list, refresh the message list, and receive an event of reconnection success.
NOTE: You can get as many messages as your calling of
fetch(_:_:)
method if your device stores enough messages. So you should make sure that you do not callfetch(_:_:)
more than you intended. We control it withloading
flag in our sample project.
// swift
override func viewDidLoad() {
messageCollection.fetch(in: SBSMMessageDirection.previous, completionHandler: { (error) in
// Fetching from cache is done
})
messageCollection.fetch(in: SBSMMessageDirection.next, completionHandler: { (error) in
// Fetching from cache is done
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// .. dequeue reusable cell
if self.channels.count > 0 && indexPath.row + 1 == self.channels.count {
messageCollection?.fetch(in: direction, completionHandler: { (error) in
// Fetching from cache is done
})
}
// ...
}
func refreshMessages() {
messageCollection?.resetViewpointTimestamp(getLastReadTimestamp())
messageCollection?.fetch(in: direction, completionHandler: { (error) in
// Fetching from cache is done
})
}
// MARK SendBird Connection Delegate
func didSucceedReconnection() {
messageCollection?.resetViewpointTimestamp(getLastReadTimestamp())
messageCollection?.fetch(in: direction, completionHandler: { (error) in
// Fetching from cache is done
})
}
// objective-c
- (void)viewDidLoad {
// ..
[messageCollection fetchInDirection:SBSMMessageDirectionPrevious completionHandler:^(SBDError * _Nullable error) {
// Fetching from cache is done
}];
[messageCollection fetchInDirection:SBSMMessageDirectionNext completionHandler:^(SBDError * _Nullable error) {
// Fetching from cache is done
}];
// ..
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// .. dequeue reusable cell
if (self.messages.count > 0 && indexPath.row + 1 == self.messages.count) {
[messageCollection fetchInDirection:direction completionHandler:^(SBDError * _Nullable error) {
// fetching from cache is done
}];
}
// ...
}
- (void)refreshMessages {
[messageCollection resetViewpointTimestamp:getLastReadTimestamp()];
[messageCollection fetchInDirection:direction completionHandler:^(SBDError * _Nullable error) {
// Fetching from cache is done
}];
}
#pragma mark - SendBird Connection Delegate
- (void)didSucceedReconnection {
[messageCollection resetViewpointTimestamp:getLastReadTimestamp()];
[messageCollection fetchInDirection:direction completionHandler:^(SBDError * _Nullable error) {
// Fetching from cache is done
}];
}
Fetched messages would be delivered to a delegate. The fetcher determines the SBSMMessageEventAction
automatically, so you don't have to consider duplicated data in view.
Handle uncaught messages
SyncManager listens to message event such as channel(_:didReceive:)
and channel(_:didUpdate:)
, and applies the change automatically. But they would not be called if the message is sent by currentUser
. You can keep track of the message by calling related function when the currentUser
sends or updates the message. SBSMMessageCollection
provides methods to apply the message event to the collections.
// swift
// call collection.appendMessage() after sending message
var previewMessage: SBDUserMessage?
channel.sendUserMessage(with: params, completionHandler: { (theMessage, theError) in
guard let message: SBDUserMessage = theMessage, let _: SBDError = theError else {
// delete preview message if sending message fails
messageCollection.deleteMessage(previewMessage)
return
}
messageCollection.appendMessage(message)
})
if let thePreviewMessage: SBDUserMessage = previewMessage {
messageCollection.appendMessage(thePreviewMessage)
}
// call collection.updateMessage() after updating message
channel.sendUserMessage(with: params, completionHandler: { (theMessage, error) in
guard let message: SBDUserMessage = theMessage, let _: SBDError = error else {
return
}
messageCollection.updateMessage(message)
})
// objective-c
// call [collection appendMessage:] after sending message
__block SBDUserMessage *previewMessage = [channel sendUserMessageWithParams:params completionHandler:^(SBDUserMessage * _Nullable userMessage, SBDError * _Nullable error) {
if (error != nil) {
[messageCollection deleteMessage:previewMessage];
return;
}
[self.messageCollection appendMessage:userMessage];
}];
if (previewMessage.requestId != nil) {
[messageCollection appendMessage:previewMessage];
}
// call [collection updateMessage:] after updating message
[channel sendUserMessageWithParams:params completionHandler:^(SBDUserMessage * _Nullable userMessage, SBDError * _Nullable error) {
[self.messageCollection updateMessage:userMessage];
}];
It works only for messages sent by currentUser
, which means the message sender should be currentUser
.
Connection lifecycle
You should let SyncManager start synchronization after being connected to Sendbird server. Call resumeSynchronization()
on connection, and pauseSynchronization()
on disconnection. Here's the code:
// swift
let manager: SBSMSyncManager = SBSMSyncManager()
manager.resumeSynchronize()
let manager: SBSMSyncManager = SBSMSyncManager()
manager.pauseSynchronize()
// objective-c
SBSMSyncManager *manager = [SBSMSyncManager manager];
[manager resumeSynchronize];
SBSMSyncManager *manager = [SBSMSyncManager manager];
[manager pauseSynchronize];
The example below shows relation of connection status and resume synchronization.
// swift
// Request Connect to Sendbird
SBDMain.connect(withUserId: userId) { (user, error) in
if let theError: NSError = error {
return
}
let manager: SBSMSyncManager = SBSMSyncManager()
manager.resumeSynchronize()
}
// Sendbird Connection Delegate
func didSucceedReconnection() {
let manager: SBSMSyncManager = SBSMSyncManager()
manager.resumeSynchronize()
}
// objective-c
// Request Connect to Sendbird
[SBDMain connectWithUserId:userId completionHandler:^(SBDUser * _Nullable user, SBDError * _Nullable error) {
if (error != nil) {
//
return;
}
SBSMSyncManager *manager = [SBSMSyncManager manager];
[manager resumeSynchronize];
}];
// Sendbird Connection Delegate
- (void)didSucceedReconnection {
SBSMSyncManager *manager = [SBSMSyncManager manager];
[manager resumeSynchronize];
}
You should choose an action after execute disconnect()
explicitly. You can clear the current user's database or stop synchronizing.
// swift
SBDMain.disconnect {
// clear cache
SBSMSyncManager().clearCache()
// stop synchronizing
SBSMSyncManager().pauseSynchronize()
}
// objective-c
[SBDMain disconnectWithCompletionHandler:^{
// clear cache
[[SBSMSyncManager manager] clearCache];
// stop synchronizing
[[SBSMSyncManager manager] pauseSynchronize];
}];
WARNING! DO NOT call
SBDMain.removeAllChannelDelegates()
. It does not only remove handlers you added, but also remove handlers managed by SyncManager.