https://github.com/sendbird/sendbird-syncmanager-ios
Sendbird SyncManager for iOS is an add-on for reliable chat data caching with Chat SDK features.
https://github.com/sendbird/sendbird-syncmanager-ios
Last synced: about 1 year ago
JSON representation
Sendbird SyncManager for iOS is an add-on for reliable chat data caching with Chat SDK features.
- Host: GitHub
- URL: https://github.com/sendbird/sendbird-syncmanager-ios
- Owner: sendbird
- License: other
- Created: 2019-01-31T09:39:32.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2023-03-08T00:44:54.000Z (over 3 years ago)
- Last Synced: 2025-04-24T06:53:59.289Z (about 1 year ago)
- Language: Objective-C
- Homepage:
- Size: 122 MB
- Stars: 5
- Watchers: 11
- Forks: 2
- Open Issues: 11
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.md
Awesome Lists containing this project
README
# [Sendbird](https://sendbird.com) SyncManager for iOS
[](https://cocoapods.org/pods/SendBirdSyncManager)
[](https://github.com/sendbird/sendbird-syncmanager-ios)
[](https://cocoapods.org/pods/SendBirdSyncManager)
[](https://github.com/Carthage/Carthage)
[](https://github.com/sendbird/sendbird-syncmanager-ios/blob/master/LICENSE.md)
## Table of contents
1. [Introduction](#introduction)
1. [Before getting started](#before-getting-started)
1. [Getting started](#getting-started)
1. [Implementation guide](#implementation-guide)
## Introduction
**Sendbird SyncManager** for iOS is a [Chat SDK](https://github.com/sendbird/sendbird-ios-framework) 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](https://sendbird.com/docs/syncmanager/v1/ios/getting-started/about-syncmanager). If you have any comments or questions regarding bugs and feature requests, visit [Sendbird community](https://community.sendbird.com).
## 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.
- https://github.com/sendbird/SyncManager-iOS-Swift
> **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](https://sendbird.com/docs/chat/v3/ios/getting-started/install-chat-sdk#2-step-1-create-a-sendbird-application-from-your-dashboard) section to learn more.
### Install SendBirdSyncManager framework from CocoaPods
Add below into your Podfile on Xcode.
```bash
platform :ios, '8.0'
use_frameworks!
target YOUR_PROJECT_TARGET do
pod 'SendBirdSyncManager'
end
```
Install `SendBirdSyncManager` framework through `CocoaPods`.
```bash
pod install
```
Update `SendBirdSyncManager` framework through `CocoaPods`.
```bash
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 `/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
// swift
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// after getting user's ID or login
SBSMSyncManager.setup(withUserId: userId)
}
```
```objc
// 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](https://github.com/sendbird/sendbird-ios-framework) 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
// 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
}
```
```objc
// 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
// swift
deinit {
channelCollection?.delegate = nil
channelCollection?.remove()
}
```
```objc
// 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
// 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
}
}
}
```
```objc
// objective-c
// add delegate
#import
@interface GroupChannelListViewController : UIViewController
@end
@implementation GroupChannelListViewController
- (void)viewDidLoad {
channelCollection.delegate = self;
// ..
}
// channel collection delegate
- (void)collection:(SBSMChannelCollection *)collection didReceiveEvent:(SBSMChannelEventAction)action channels:(NSArray *)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
// 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
})
}
// ...
}
```
```objc
// 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
// 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)
// ...
}
```
```objc
// 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
// swift
deinit {
messageCollection?.delegate = nil
messageCollection?.remove()
}
```
```objc
- (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](https://github.com/sendbird/sendbird-ios-framework).
```swift
// 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
}
}
}
```
```objc
// objective-c
// add delegate
#import
@interface GroupChannelChattingViewController : UIViewController
@end
@implementation GroupChannelChattingViewController
- (void)viewDidLoad {
// ..
messageCollection.delegate = self;
// ..
}
// message collection delegate
- (void)collection:(SBSMMessageCollection *)collection didReceiveEvent:(SBSMMessageEventAction)action messages:(NSArray *)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
// 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
})
}
```
```objc
// 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
// 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)
})
```
```objc
// 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
// swift
let manager: SBSMSyncManager = SBSMSyncManager()
manager.resumeSynchronize()
let manager: SBSMSyncManager = SBSMSyncManager()
manager.pauseSynchronize()
```
```objc
// 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
// 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()
}
```
```objc
// 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
// swift
SBDMain.disconnect {
// clear cache
SBSMSyncManager().clearCache()
// stop synchronizing
SBSMSyncManager().pauseSynchronize()
}
```
```objc
// 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.