Skip to content

Sendbird SyncManager for iOS is an add-on for reliable chat data caching with Chat SDK features.

License

Notifications You must be signed in to change notification settings

sendbird/sendbird-syncmanager-ios

Repository files navigation

Sendbird SyncManager for iOS

Platform Languages CocoaPods Carthage compatible Commercial License

Table of contents

  1. Introduction
  2. Before getting started
  3. Getting started
  4. Implementation guide

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 with SendBird SDK. If you install SendBirdSyncManager, Cocoapods automatically install SendBird SDK as well. And the minimum version of SendBird SDK is 3.0.203.

Install SendBirdSyncManager framework from Carthage

  1. Add github "sendbird/sendbird-syncmanager-ios" to your Cartfile.
  2. Run carthage update.
  3. Go to your Xcode project's General settings. Open <YOUR_XCODE_PROJECT_DIRECTORY>/Carthage/Build/iOS in Finder and drag SendBirdSyncManager.framework to the Embedded Binaries section in Xcode. Make sure Copy 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:

  1. It loads channels from cache and the view shows them.
  2. Then it fetches the most recent channels from Sendbird server and merges with the channels in view.
  3. 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 call fetch(_:_:) more than you intended. We control it with loading 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.

About

Sendbird SyncManager for iOS is an add-on for reliable chat data caching with Chat SDK features.

Resources

License

Stars

Watchers

Forks

Packages

No packages published